diff --git a/.gitignore b/.gitignore index 0f904f2d..e0825869 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ results !/ansible/environments/mac # actionloop action compiler script +!core/nodejsActionBase/bin/compile !core/typescript37Action/bin/compile # Eclipse diff --git a/.travis.yml b/.travis.yml index 494870d0..4c306084 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,13 +22,6 @@ language: java services: - docker -notifications: - email: false - webhooks: - urls: - # travis2slack webhook to enable DMs on openwhisk-team.slack.com to PR authors with TravisCI results - secure: "C+xu3CoAqyHm8N8zVwjOGhZOxYmwjZRkBMZUAbiNz6vVZq6j/TU8Mu44Z5zEWsufSdrAtu+Mg4Kfr00x8hlYYBxH6YXs7vKhToCERY1JLnlSjquvzzbOkaiG+fpYyiATNH6uz1BJdm1FcSr8n2mRV1nmAXmE/Ie28Q+nVIFt8CXA/mBWl9Z/l6Rwf7REWgWL4mGr2Wjb3vgth0IEO7rZtDKxQlas0s8naJA6nA6dIPxBFuuBrLmNIRAF7qOLBLC6508UWqt+9k/QYbujM7qhnmSJEPGHXG/5lZBWbn2UPIS6EKTlYW5o44cGmP8N0MAKDJezmDjE4r8HwTiKzaM8axQPLw+H7wumhIXbPO2lsHGgzxZAh7M771cXf8pRxoaqWm/bxweAPWA+6bKSrHJGYtM9FTHghNVeuwLTR18NJj4mIzb9BS90rodLfLXrbUPY5lC/4I5YRrCqlhyHr7aibWXM27ehf5/ujkQkBSyd6LeiONl04jFcCNPHlYxIf2XJnHX/6CiE/eJ2R7xqwtGBkTXCb63hnOqDv49nDrj0PfPi2Y0B4x/zoXz6P7ZyogDM6dOOUeL2u/XBryJgQxnoH2S+7Gh+qSdAigCsD2onsF6ghHCdeQRVX/f7CHsyXVxqxHdfyXNXrJJ1uz0tVKZkNSSOwU/1JJbiA+wzGYTnu3E=" - before_install: - "./tools/travis/setup.sh" install: true @@ -36,18 +29,13 @@ script: - "./tools/travis/build.sh && ./tools/travis/test.sh" deploy: - provider: script - script: "./tools/travis/publish.sh openwhisk ${TRAVIS_TAG%@*} ${TRAVIS_TAG##*@}" + script: "./tools/travis/publish.sh nimbella nodejs10Action ${TRAVIS_TAG} && ./tools/travis/publish.sh nimbella nodejs12Action ${TRAVIS_TAG} && ./tools/travis/publish.sh nimbella nodejs14Action ${TRAVIS_TAG} && ./tools/travis/publish.sh nimbella typescript37Action ${TRAVIS_TAG}" on: tags: true all_branches: true - repo: apache/openwhisk-runtime-nodejs + repo: nimbella-corp/openwhisk-runtime-nodejs - provider: script - script: "./tools/travis/publish.sh openwhisk nodejs10Action nightly && ./tools/travis/publish.sh openwhisk nodejs12Action nightly && ./tools/travis/publish.sh openwhisk nodejs14Action nightly && ./tools/travis/publish.sh openwhisk typescript37Action nightly" + script: "./tools/travis/publish.sh nimbella nodejs10Action nightly && ./tools/travis/publish.sh nimbella nodejs12Action nightly && ./tools/travis/publish.sh nimbella nodejs14Action nightly && ./tools/travis/publish.sh nimbella typescript37Action nightly" on: - branch: master - repo: apache/openwhisk-runtime-nodejs - -env: - global: - - secure: Y1ldwIQ6bc3/3Pc0E+qQ6K2M830B9BObYDlsNilPwF/kak3YSfF7SuXuRbJGjTdhH2KOotZD2CwONgP2yvOSPBToC/HpnXYfAGtgblrxQORvgdik88CFWa3Lli1pwlpdzKQNWhBvglzq+IIS98wqzmwqGr8zKA+Iau2ByHdb1j3M9rrIY9V6oU9Gwim1apcRyfI/as3+QfPtt8BUAl2U7+PprxwJigyF/mcZnBJbd7IjrilE2gldZLxKlBiffoKVBinrEg3IQGJPt6k8riw264pBQEpcA0ZBsPUvMaISSxLb+d1ymp3WsiTJUjv+URR/HcdDa7P9jY+ouc8PQz4Yt+Ii38lM2tQU480APfVTyfj6drkjL/+54mYuxm8TzkBWcM2j6/FYT+8HvK/pF35wDJ3El+jGq7BARXg8HVxFsZgynJnhqhWDQb3xX9fK+N4K8+ct+HlsOSa5mP5i5Yo6WRTrWrFpyxVnv9crKePCiYqA2Y8ta8Wnh0sM06nLRtDbfbDjvXPQbaZqSnL4B2Cto08YoT/W/lu8QgJ3EEFlUdDOke4kv9yoXtuE0h7+8dwOvBNMVrBis3G2EYObgR4WmWjk4loYhqT9h3jrH0/5bGLzSKc++qYW0rOZ/RB21cRSe1ILQvSzWkImUoPI8b0i5baGSDq3EjTXYr3pIXSYpQk= - - secure: DOg+FgllLbyv7nEK3JJZfO/cvXy5K0L6QI/S9EJ/ivm4XBDCw1ayhrSQXvp1tMTPbWBEIv2gomPsHghJ+hVvX3dgwYdoNz9WZaNBB6lOO9U8OQW0LBsO5Eai6grzqOP35OuKtthuyR3dGJHAZo/XjhZM/jL0z6q1kNDzdS7ASwRwHJG0rHPGVlGeolH4nAity4KNJvyAspS1FYaIj9FEC/M7UT6nJVACbr9iMt/83teF/Oo2uoFI6Pa4K3nE2NViVFibToNOM3CV8kArDPDoNJviXxQ07ZM6fNijwehZ80waiPSaxFY8PLSntQNxGyB3DbomSTCcdVvtuHHQVmZgpVdvOJE8wk3R09+nq9U9FuUWLiRYSRbF1eF48YFnssPW1jEeVSenFRADcQ37e4B21ssLvXRHpQHpPVrYBZ8ffDamS7pKtEqocX/3Syc6irxHGCpxEdhvQ0Of5AWHhzB714VCijJJQiH9J4hEKXhMeAZ5SSt4eUavEHJKMhUcJ/aaku5w1+KtiYeOso1fTRbTYkEYb1A0bSNdXlGrYRRT+N683W7+ENiQIT+hTp617L/m5WQLIHfKe3gy5qt46kHFiUL3R3YlBI5OKjnLkDnUiA+0d4SFtJC/TBuPmH1fG4isAuvggTjzCb3kUG3ysin8AW0AKaXW0eqjKa/nsHWDKmc= + branch: dev + repo: nimbella-corp/openwhisk-runtime-nodejs diff --git a/README.md b/README.md index 1ecaef9c..9cbe8bfb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ # Apache OpenWhisk runtimes for Node.js [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) -[![Build Status](https://travis-ci.com/apache/openwhisk-runtime-nodejs.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-nodejs) +[![Build Status](https://travis-ci.com/nimbella-corp/openwhisk-runtime-nodejs.svg?branch=master)](https://travis-ci.com/nimbella-corp/openwhisk-runtime-nodejs) This repository contains sources files needed to build the Node.js runtimes for Apache OpenWhisk. The build system will produce a series of docker images for each runtime version. These images are used in the platform to execute Node.js actions. diff --git a/core/nodejs10Action/CHANGELOG.md b/core/nodejs10Action/CHANGELOG.md deleted file mode 100644 index ec5cf657..00000000 --- a/core/nodejs10Action/CHANGELOG.md +++ /dev/null @@ -1,53 +0,0 @@ - - -# NodeJS 10 OpenWhisk Runtime Container - -# Next Release -Node.js version = [10.23.2](https://nodejs.org/en/blog/release/v10.23.2/) - -## Apache 1.17 - - Update Node.js and OpenWhisk versions. - -Node.js version = [10.23.0](https://nodejs.org/en/blog/release/v10.23.0/) -OpenWhisk version = [OpenWhisk v3.21.3](https://www.npmjs.com/package/openwhisk) - -## Apache 1.16 -Changes: - - Update Node.js and OpenWhisk versions. - -Node.js version = [10.21.0](https://nodejs.org/en/blog/release/v10.21.0/) -OpenWhisk version = [OpenWhisk v3.21.2](https://www.npmjs.com/package/openwhisk) - -## Apache 1.15 -Changes: - - Update Node.js - - Update OpenWhisk npm package - - Support for __OW_ACTION_VERSION (openwhisk/4761) - -Node.js version = [10.19.0](https://nodejs.org/en/blog/release/v10.19.0/) -OpenWhisk version = [OpenWhisk v3.21.1](https://www.npmjs.com/package/openwhisk) - -## Apache 1.13 -Changes: -- Initial version with NodejS10 LTS -- Node.js version = [10.16.3](https://nodejs.org/en/blog/release/v10.16.3/) -- [OpenWhisk v3.18.0](https://www.npmjs.com/package/openwhisk) - JavaScript client library for the OpenWhisk platform. Provides a wrapper around the OpenWhisk APIs. - -Node.js version = [10.15.3](https://nodejs.org/en/blog/release/v10.15.3/) diff --git a/core/nodejs10Action/Dockerfile b/core/nodejs10Action/Dockerfile deleted file mode 100644 index 36ca2472..00000000 --- a/core/nodejs10Action/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -FROM node:10.23.2-stretch - -# Initial update and some basics. -# -RUN apt-get update && apt-get install -y \ - imagemagick \ - graphicsmagick \ - unzip \ - && rm -rf /var/lib/apt/lists/* - -# Add sources and copy the package.json to root container, -# so npm packages from user functions take precendence. -# -WORKDIR /nodejsAction -ADD . /nodejsAction/ -COPY package.json / - -# Customize runtime with additional packages. -# Install package globally so user packages can override. -# -RUN cd / && npm install --no-package-lock --production \ - && npm cache clean --force - -EXPOSE 8080 - -# The flag --experimental-worker enables worker threads, -# see https://nodejs.org/docs/latest-v10.x/api/worker_threads.html -CMD node --experimental-worker --expose-gc app.js diff --git a/core/nodejs10Action/knative/Dockerfile b/core/nodejs10Action/knative/Dockerfile deleted file mode 100644 index 2453c2bb..00000000 --- a/core/nodejs10Action/knative/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -FROM node:10.20.1-stretch -RUN apt-get update && apt-get install -y \ - imagemagick \ - graphicsmagick \ - unzip \ - && rm -rf /var/lib/apt/lists/* -WORKDIR /nodejsAction -# COPY source code "*.js" from nodejsActionBase to current working dir -RUN mkdir /src /platform -COPY ./core/nodejsActionBase/*.js ./ -COPY ./core/nodejsActionBase/src/*.js ./src/ -COPY ./core/nodejsActionBase/platform/*.js ./platform/ -COPY . . -# COPY the package.json to root container, so we can install npm packages a level up from user's packages, -# so user's packages take precedence -COPY ./core/nodejsActionBase/package.json / -RUN cd / && npm install --no-package-lock \ - && npm cache clean --force -EXPOSE 8080 -# The flag --experimental-worker enables worker threads, see https://nodejs.org/docs/latest-v10.x/api/worker_threads.html -CMD node --experimental-worker --expose-gc app.js diff --git a/core/nodejs12Action/.dockerignore b/core/nodejs12Action/.dockerignore deleted file mode 100644 index a1d03cb9..00000000 --- a/core/nodejs12Action/.dockerignore +++ /dev/null @@ -1,13 +0,0 @@ -*.*~ -*.yaml -*.tmpl -*.gradle -.dockerignore -.project -.settings -build.xml -Dockerfile -logs -node_modules -package-lock.json -test.js diff --git a/core/nodejs12Action/CHANGELOG.md b/core/nodejs12Action/CHANGELOG.md deleted file mode 100644 index 58d60200..00000000 --- a/core/nodejs12Action/CHANGELOG.md +++ /dev/null @@ -1,54 +0,0 @@ - - -# NodeJS 12 OpenWhisk Runtime Container - -# Next Release -Node.js version = [12.20.1](https://nodejs.org/en/blog/release/v12.20.1/) - -## Apache 1.17 - - Update Node.js and OpenWhisk versions. - -Node.js version = [12.19.1](https://nodejs.org/en/blog/release/v12.19.1/) -OpenWhisk version = [OpenWhisk v3.21.3](https://www.npmjs.com/package/openwhisk) - -## Apache 1.16 -Changes: - - Update Node.js and OpenWhisk versions. - - Update OpenWhisk npm package - -Node.js version = [12.18.2](https://nodejs.org/en/blog/release/v12.18.2/) -OpenWhisk version = [OpenWhisk v3.21.2](https://www.npmjs.com/package/openwhisk) - -## Apache 1.15 -Changes: - - Update Node.js - - Update OpenWhisk npm package - - Support for __OW_ACTION_VERSION (openwhisk/4761) - -Node.js version = [12.15.0](https://nodejs.org/en/blog/release/v12.15.0/) -OpenWhisk version = [OpenWhisk v3.21.1](https://www.npmjs.com/package/openwhisk) - -## Apache 1.14 -Changes: -- Adding Nodejs version 12 -- Node.js version = [12.8.1](https://nodejs.org/en/blog/release/v12.8.1/) -- [OpenWhisk v3.18.0](https://www.npmjs.com/package/openwhisk) - JavaScript client library for the OpenWhisk platform. Provides a wrapper around the OpenWhisk APIs. - -Node.js version = [12.1.0](https://nodejs.org/en/blog/release/v12.1.0/) diff --git a/core/nodejs12Action/Dockerfile b/core/nodejs12Action/Dockerfile deleted file mode 100644 index 0558f92d..00000000 --- a/core/nodejs12Action/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -FROM node:12.20.1-stretch - -# Initial update and some basics. -# -RUN apt-get update && apt-get install -y \ - imagemagick \ - graphicsmagick \ - unzip \ - && rm -rf /var/lib/apt/lists/* - -# Add sources and copy the package.json to root container, -# so npm packages from user functions take precendence. -# -WORKDIR /nodejsAction -ADD . /nodejsAction/ -COPY package.json / - -# Customize runtime with additional packages. -# Install package globally so user packages can override. -# -RUN cd / && npm install --no-package-lock --production \ - && npm cache clean --force - -EXPOSE 8080 - -CMD node --expose-gc app.js diff --git a/core/nodejs14Action/Dockerfile b/core/nodejs14Action/Dockerfile index 80fd805e..658c4cbd 100644 --- a/core/nodejs14Action/Dockerfile +++ b/core/nodejs14Action/Dockerfile @@ -15,29 +15,78 @@ # limitations under the License. # -FROM node:14.15.4-stretch +# build go proxy from source +ARG GO_PROXY_BASE_IMAGE=golang:1.18 +FROM $GO_PROXY_BASE_IMAGE AS builder_source +ARG GO_PROXY_GITHUB_USER=nimbella-corp +ARG GO_PROXY_GITHUB_BRANCH=dev +RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} \ + https://github.com/${GO_PROXY_GITHUB_USER}/openwhisk-runtime-go /src ;\ + cd /src ; env GO111MODULE=on CGO_ENABLED=0 go build main/proxy.go && \ + mv proxy /bin/proxy + +# or build it from a release +FROM $GO_PROXY_BASE_IMAGE AS builder_release +ARG GO_PROXY_RELEASE_VERSION=1.15@1.17.0 +RUN curl -sL \ + https://github.com/apache/openwhisk-runtime-go/archive/{$GO_PROXY_RELEASE_VERSION}.tar.gz\ + | tar xzf -\ + && cd openwhisk-runtime-go-*/main\ + && GO111MODULE=on go build -o /bin/proxy + +FROM node:14-stretch + +# select the builder to use +ARG GO_PROXY_BUILD_FROM=source # Initial update and some basics. -# -RUN apt-get update && apt-get install -y \ - imagemagick \ - graphicsmagick \ - unzip \ - && rm -rf /var/lib/apt/lists/* +# Replace debian sources with debian archive since stretch has been pulled from the main repos. +RUN sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list \ + && sed -i 's|security.debian.org|archive.debian.org/|g' /etc/apt/sources.list \ + && sed -i '/stretch-updates/d' /etc/apt/sources.list \ + && apt-get update && apt-get install -y \ + imagemagick \ + graphicsmagick \ + unzip \ + && rm -rf /var/lib/apt/lists/* -# Add sources and copy the package.json to root container, +# Copy the package.json to root container, # so npm packages from user functions take precendence. -# WORKDIR /nodejsAction -ADD . /nodejsAction/ COPY package.json / # Customize runtime with additional packages. # Install package globally so user packages can override. -# -RUN cd / && npm install --no-package-lock --production \ - && npm cache clean --force +RUN cd / \ + && npm install --no-package-lock --omit=dev \ + && npm cache clean --force + +# Copy sources in after copying in package.json and running npm install to +# enable faster builds after only sources change. +COPY . /nodejsAction/ + +# install the functions-deployer +ARG DEPLOYER_DOWNLOAD +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN curl -L ${DEPLOYER_DOWNLOAD} | tar xzf - \ + && rm -fr /usr/local/lib/dosls && mv dosls /usr/local/lib \ + && rm -f /usr/local/bin/dosls && ln -s /usr/local/lib/dosls/bootstrap /usr/local/bin/dosls + +ARG __OW_LAMBDA_COMPAT +ENV __OW_LAMBDA_COMPAT=$__OW_LAMBDA_COMPAT + +COPY --from=builder_source /bin/proxy /bin/proxy_source +COPY --from=builder_release /bin/proxy /bin/proxy_release +RUN mv /bin/proxy_${GO_PROXY_BUILD_FROM} /bin/proxy + +ADD bin/compile /bin/compile +ENV OW_COMPILER=/bin/compile + +# log initialization errors +ENV OW_LOG_INIT_ERROR=1 +# the launcher must wait for an ack +ENV OW_WAIT_FOR_ACK=1 -EXPOSE 8080 +ENV OW_INIT_IN_ACTIONLOOP=/nodejsAction/prelauncher.js -CMD node --expose-gc app.js +ENTRYPOINT [ "/bin/proxy" ] diff --git a/core/nodejs14Action/build.gradle b/core/nodejs14Action/build.gradle index 85e07966..8aabac77 100644 --- a/core/nodejs14Action/build.gradle +++ b/core/nodejs14Action/build.gradle @@ -27,13 +27,8 @@ ext.dockerImageName = 'action-nodejs-v14' apply from: '../../gradle/docker.gradle' distDocker.dependsOn 'copyPackageJson' -distDocker.dependsOn 'copyProxy' distDocker.dependsOn 'copyRunner' -distDocker.dependsOn 'copyService' -distDocker.dependsOn 'copyPlatform' -distDocker.dependsOn 'copyOpenWhisk' -distDocker.dependsOn 'copyKnative' -distDocker.dependsOn 'copyBuildTemplate' +distDocker.dependsOn 'copyCompile' distDocker.finalizedBy('cleanup') task copyPackageJson(type: Copy) { @@ -41,46 +36,31 @@ task copyPackageJson(type: Copy) { into '.' } -task copyProxy(type: Copy) { - from '../nodejsActionBase/app.js' - into '.' -} - task copyRunner(type: Copy) { from '../nodejsActionBase/runner.js' into '.' -} - -task copyService(type: Copy) { - from '../nodejsActionBase/src/service.js' - into './src' -} -task copyPlatform(type: Copy) { - from '../nodejsActionBase/platform/platform.js' - into './platform' -} + from '../nodejsActionBase/lambda.js' + into '.' -task copyOpenWhisk(type: Copy) { - from '../nodejsActionBase/platform/openwhisk.js' - into './platform' -} + from '../nodejsActionBase/nim.js' + into '.' -task copyKnative(type: Copy) { - from '../nodejsActionBase/platform/knative.js' - into './platform' + from '../nodejsActionBase/launcher.js' + into '.' } -task copyBuildTemplate(type: Copy) { - from '../nodejsActionBase/buildtemplate.yaml' - into '.' +task copyCompile(type: Copy) { + from '../nodejsActionBase/bin/compile' + into './bin' } task cleanup(type: Delete) { delete 'package.json' - delete 'app.js' + delete 'launcher.js' + delete 'lambda.js' + delete 'nim.js' delete 'runner.js' delete 'src' - delete 'platform' - delete 'buildtemplate.yaml' + delete 'bin' } diff --git a/core/nodejs10Action/.dockerignore b/core/nodejs18Action/.dockerignore similarity index 100% rename from core/nodejs10Action/.dockerignore rename to core/nodejs18Action/.dockerignore diff --git a/core/nodejs18Action/Dockerfile b/core/nodejs18Action/Dockerfile new file mode 100644 index 00000000..f9767fe5 --- /dev/null +++ b/core/nodejs18Action/Dockerfile @@ -0,0 +1,87 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# build go proxy from source +ARG GO_PROXY_BASE_IMAGE=golang:1.18 +FROM $GO_PROXY_BASE_IMAGE AS builder + +ARG GO_PROXY_GITHUB_USER=nimbella-corp +ARG GO_PROXY_GITHUB_BRANCH=dev +RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} https://github.com/${GO_PROXY_GITHUB_USER}/openwhisk-runtime-go /src \ + && cd /src \ + && env GO111MODULE=on CGO_ENABLED=0 go build -o /bin/proxy main/proxy.go + +FROM node:18-bullseye-slim + +# Initial update and some basics. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + # For npm to work properly. + git \ + ssh \ + ca-certificates \ + # For the proxy. + unzip \ + python \ + # For function-deployer install + curl \ + # Dependencies for the users. + graphicsmagick \ + imagemagick \ + && update-ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Copy the package.json to root container, +# so npm packages from user functions take precendence. +WORKDIR /nodejsAction +COPY package.json / + +# Customize runtime with additional packages. +# Install package globally so user packages can override. +RUN cd / \ + # Pinning npm to version 8 as 9 has known issues around UID. + # See https://github.com/npm/cli/issues/5889 for example. + && npm i -g npm@8 \ + && npm install --no-package-lock --omit=dev \ + && npm cache clean --force + +# Copy sources in after copying in package.json and running npm install to +# enable faster builds after only sources change. +COPY . /nodejsAction/ + +# install the functions-deployer +ARG DEPLOYER_DOWNLOAD +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN curl -L ${DEPLOYER_DOWNLOAD} | tar xzf - \ + && rm -fr /usr/local/lib/dosls && mv dosls /usr/local/lib \ + && rm -f /usr/local/bin/dosls && ln -s /usr/local/lib/dosls/bootstrap /usr/local/bin/dosls + +ARG __OW_LAMBDA_COMPAT +ENV __OW_LAMBDA_COMPAT=$__OW_LAMBDA_COMPAT + +COPY bin/compile /bin/compile +ENV OW_COMPILER=/bin/compile + +# log initialization errors +ENV OW_LOG_INIT_ERROR=1 +# the launcher must wait for an ack +ENV OW_WAIT_FOR_ACK=1 + +ENV OW_INIT_IN_ACTIONLOOP=/nodejsAction/prelauncher.js + +COPY --from=builder /bin/proxy /bin/proxy +ENTRYPOINT [ "/bin/proxy" ] \ No newline at end of file diff --git a/core/nodejs12Action/build.gradle b/core/nodejs18Action/build.gradle similarity index 61% rename from core/nodejs12Action/build.gradle rename to core/nodejs18Action/build.gradle index ae773f6d..04b067a0 100644 --- a/core/nodejs12Action/build.gradle +++ b/core/nodejs18Action/build.gradle @@ -23,17 +23,12 @@ eclipse { } } -ext.dockerImageName = 'action-nodejs-v12' +ext.dockerImageName = 'action-nodejs-v18' apply from: '../../gradle/docker.gradle' distDocker.dependsOn 'copyPackageJson' -distDocker.dependsOn 'copyProxy' distDocker.dependsOn 'copyRunner' -distDocker.dependsOn 'copyService' -distDocker.dependsOn 'copyPlatform' -distDocker.dependsOn 'copyOpenWhisk' -distDocker.dependsOn 'copyKnative' -distDocker.dependsOn 'copyBuildTemplate' +distDocker.dependsOn 'copyCompile' distDocker.finalizedBy('cleanup') task copyPackageJson(type: Copy) { @@ -41,46 +36,31 @@ task copyPackageJson(type: Copy) { into '.' } -task copyProxy(type: Copy) { - from '../nodejsActionBase/app.js' - into '.' -} - task copyRunner(type: Copy) { from '../nodejsActionBase/runner.js' into '.' -} - -task copyService(type: Copy) { - from '../nodejsActionBase/src/service.js' - into './src' -} -task copyPlatform(type: Copy) { - from '../nodejsActionBase/platform/platform.js' - into './platform' -} + from '../nodejsActionBase/lambda.js' + into '.' -task copyOpenWhisk(type: Copy) { - from '../nodejsActionBase/platform/openwhisk.js' - into './platform' -} + from '../nodejsActionBase/nim.js' + into '.' -task copyKnative(type: Copy) { - from '../nodejsActionBase/platform/knative.js' - into './platform' + from '../nodejsActionBase/launcher.js' + into '.' } -task copyBuildTemplate(type: Copy) { - from '../nodejsActionBase/buildtemplate.yaml' - into '.' +task copyCompile(type: Copy) { + from '../nodejsActionBase/bin/compile' + into './bin' } task cleanup(type: Delete) { delete 'package.json' - delete 'app.js' + delete 'launcher.js' + delete 'lambda.js' + delete 'nim.js' delete 'runner.js' delete 'src' - delete 'platform' - delete 'buildtemplate.yaml' + delete 'bin' } diff --git a/core/nodejs22Action/Dockerfile b/core/nodejs22Action/Dockerfile new file mode 100644 index 00000000..2a3ce91d --- /dev/null +++ b/core/nodejs22Action/Dockerfile @@ -0,0 +1,84 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# build go proxy from source +ARG GO_PROXY_BASE_IMAGE=golang:1.22 +FROM $GO_PROXY_BASE_IMAGE AS builder + +ARG GO_PROXY_GITHUB_USER=nimbella-corp +ARG GO_PROXY_GITHUB_BRANCH=dev +RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} https://github.com/${GO_PROXY_GITHUB_USER}/openwhisk-runtime-go /src \ + && cd /src \ + && env GO111MODULE=on CGO_ENABLED=0 go build -o /bin/proxy main/proxy.go + +FROM node:22-bookworm-slim + +# Initial update and some basics. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + # For npm to work properly + git \ + ssh \ + ca-certificates \ + # For the proxy. + unzip \ + python3 \ + # For function-deployer install + curl \ + # Dependencies for the users. + graphicsmagick \ + imagemagick \ + && update-ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Copy the package.json to root container, +# so npm packages from user functions take precendence. +WORKDIR /nodejsAction +COPY package.json / + +# Customize runtime with additional packages. +# Install package globally so user packages can override. +RUN cd / \ + && npm install --no-package-lock --omit=dev \ + && npm cache clean --force + +# Copy sources in after copying in package.json and running npm install to +# enable faster builds after only sources change. +COPY . /nodejsAction/ + +# install the functions-deployer +ARG DEPLOYER_DOWNLOAD +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN curl -L ${DEPLOYER_DOWNLOAD} | tar xzf - \ + && rm -fr /usr/local/lib/dosls && mv dosls /usr/local/lib \ + && rm -f /usr/local/bin/dosls && ln -s /usr/local/lib/dosls/bootstrap /usr/local/bin/dosls + +ARG __OW_LAMBDA_COMPAT +ENV __OW_LAMBDA_COMPAT=$__OW_LAMBDA_COMPAT + +COPY bin/compile /bin/compile +ENV OW_COMPILER=/bin/compile + +# log initialization errors +ENV OW_LOG_INIT_ERROR=1 +# the launcher must wait for an ack +ENV OW_WAIT_FOR_ACK=1 + +ENV OW_INIT_IN_ACTIONLOOP=/nodejsAction/prelauncher.js + +COPY --from=builder /bin/proxy /bin/proxy +ENTRYPOINT [ "/bin/proxy" ] diff --git a/core/nodejs10Action/build.gradle b/core/nodejs22Action/build.gradle similarity index 61% rename from core/nodejs10Action/build.gradle rename to core/nodejs22Action/build.gradle index b52bdaff..4e17805e 100644 --- a/core/nodejs10Action/build.gradle +++ b/core/nodejs22Action/build.gradle @@ -23,17 +23,12 @@ eclipse { } } -ext.dockerImageName = 'action-nodejs-v10' +ext.dockerImageName = 'action-nodejs-v22' apply from: '../../gradle/docker.gradle' distDocker.dependsOn 'copyPackageJson' -distDocker.dependsOn 'copyProxy' distDocker.dependsOn 'copyRunner' -distDocker.dependsOn 'copyService' -distDocker.dependsOn 'copyPlatform' -distDocker.dependsOn 'copyOpenWhisk' -distDocker.dependsOn 'copyKnative' -distDocker.dependsOn 'copyBuildTemplate' +distDocker.dependsOn 'copyCompile' distDocker.finalizedBy('cleanup') task copyPackageJson(type: Copy) { @@ -41,46 +36,31 @@ task copyPackageJson(type: Copy) { into '.' } -task copyProxy(type: Copy) { - from '../nodejsActionBase/app.js' - into '.' -} - task copyRunner(type: Copy) { from '../nodejsActionBase/runner.js' into '.' -} - -task copyService(type: Copy) { - from '../nodejsActionBase/src/service.js' - into './src' -} -task copyPlatform(type: Copy) { - from '../nodejsActionBase/platform/platform.js' - into './platform' -} + from '../nodejsActionBase/lambda.js' + into '.' -task copyOpenWhisk(type: Copy) { - from '../nodejsActionBase/platform/openwhisk.js' - into './platform' -} + from '../nodejsActionBase/nim.js' + into '.' -task copyKnative(type: Copy) { - from '../nodejsActionBase/platform/knative.js' - into './platform' + from '../nodejsActionBase/launcher.js' + into '.' } -task copyBuildTemplate(type: Copy) { - from '../nodejsActionBase/buildtemplate.yaml' - into '.' +task copyCompile(type: Copy) { + from '../nodejsActionBase/bin/compile' + into './bin' } task cleanup(type: Delete) { delete 'package.json' - delete 'app.js' + delete 'launcher.js' + delete 'lambda.js' + delete 'nim.js' delete 'runner.js' delete 'src' - delete 'platform' - delete 'buildtemplate.yaml' + delete 'bin' } diff --git a/core/nodejsActionBase/Dockerfile b/core/nodejsActionBase/Dockerfile index d11e890a..ad0d6f5c 100644 --- a/core/nodejsActionBase/Dockerfile +++ b/core/nodejsActionBase/Dockerfile @@ -36,7 +36,13 @@ COPY package.json / RUN cd / && npm install --no-package-lock --production \ && npm cache clean --force +# move nim sdk to node modules directory so that it can be found by node module loader +RUN mkdir /node_modules/nim && mv /nodejsAction/nim.js /node_modules/nim/index.js + +ARG __LAMBDA_COMPAT +ENV __LAMBDA_COMPAT=$__LAMBDA_COMPAT + EXPOSE 8080 WORKDIR /nodejsAction -CMD node --expose-gc app.js +CMD node --expose-gc /nodejsAction/app.js diff --git a/core/nodejsActionBase/app.js b/core/nodejsActionBase/app.js deleted file mode 100644 index c50647d1..00000000 --- a/core/nodejsActionBase/app.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// __OW_ALLOW_CONCURRENT: see docs/concurrency.md -var config = { - 'port': 8080, - 'apiHost': process.env.__OW_API_HOST, - 'allowConcurrent': process.env.__OW_ALLOW_CONCURRENT, - 'requestBodyLimit': "48mb" -}; - -var bodyParser = require('body-parser'); -var express = require('express'); - -/** - * instantiate app as an instance of Express - * i.e. app starts the server - */ -var app = express(); - -/** - * instantiate an object which handles REST calls from the Invoker - */ -var service = require('./src/service').getService(config); - -/** - * setup a middleware layer to restrict the request body size - * this middleware is called every time a request is sent to the server - */ -app.use(bodyParser.json({ limit: config.requestBodyLimit })); - -// identify the target Serverless platform -const platformFactory = require('./platform/platform.js'); -const factory = new platformFactory(app, config, service); -var targetPlatform = process.env.__OW_RUNTIME_PLATFORM; - -// default to "openwhisk" platform initialization if not defined -// TODO export isvalid() from platform, if undefined this is OK to default, but if not valid value then error out -if (typeof targetPlatform === "undefined") { - targetPlatform = platformFactory.PLATFORM_OPENWHISK; - // console.log("__OW_RUNTIME_PLATFORM is undefined; defaulting to 'openwhisk' ..."); -} - -if (!platformFactory.isSupportedPlatform(targetPlatform)) { - console.error("__OW_RUNTIME_PLATFORM ("+targetPlatform+") is not supported by the runtime."); - process.exit(9); -} - -/** - * Register different endpoint handlers depending on target PLATFORM and its expected behavior. - * In addition, register request pre-processors and/or response post-processors as needed - * to move data where the platform and function author expects it to be. - */ - -const platformImpl = factory.createPlatformImpl(targetPlatform); - -if (typeof platformImpl !== "undefined") { - - platformImpl.registerHandlers(app, platformImpl); - - // short-circuit any requests to invalid routes (endpoints) that we have no handlers for. - app.use(function (req, res, next) { - res.status(500).json({error: "Bad request."}); - }); - - /** - * Register a default error handler. This effectively only gets called when invalid JSON is received - * (JSON Parser) and we do not wish the default handler to error with a 400 and send back HTML in the - * body of the response. - */ - app.use(function (err, req, res, next) { - console.log(err.stackTrace); - res.status(500).json({error: "Bad request."}); - }); - - service.start(app); - -} else { - console.error("Failed to initialize __OW_RUNTIME_PLATFORM ("+targetPlatform+")."); - process.exit(10); -} diff --git a/core/nodejsActionBase/bin/compile b/core/nodejsActionBase/bin/compile new file mode 100755 index 00000000..f734f56a --- /dev/null +++ b/core/nodejsActionBase/bin/compile @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import print_function +import os +import sys +import codecs + +def sources(launcher, source_dir, main): + # We're dealing with a zipped action if there's more than one file after unzipping + # or if we have just one but that isn't exec. + num_files = len([name for name in os.listdir(source_dir) if os.path.isfile("%s/%s" % (source_dir, name))]) + if num_files == 1 and os.path.isfile("%s/exec" % source_dir): + main_file = "main" + main_func = main + + # If we only upload a single file, it'll be called 'exec' which we need the launcher + # script to be. + src = "%s/exec" % source_dir + dst = "%s/main.js" % source_dir + body = "" + with codecs.open(src, 'r', 'utf-8') as s: + body = s.read() + with codecs.open(dst, 'w', 'utf-8') as d: + func = '''module.exports.%(main)s = (function(){ + %(code)s + try { + return %(main)s + } catch (e) { + if (e.name === 'ReferenceError') { + return module.exports.%(main)s || exports.%(main)s + } else throw e + } +})()''' % {"code": body, "main": main} + d.write(func) + else: + # main can be either + # file.func + # func + # + # If only 1 part is defined, Node.js will infer the file to either be index.js or + # part of package.json. + parts = main.split(".", 1) + if len(parts) == 1: + if not os.path.isfile("%s/index.js" % source_dir) and not os.path.isfile("%s/index.mjs" % source_dir) and not os.path.isfile("%s/package.json" % source_dir): + print("Zipped functions must contain either package.json or index.[m]js at the root.") + sys.exit(1) + + main_file = "" + main_func = parts[0] + else: + main_file = parts[0] + main_func = parts[1] + + # Write the launcher file to the current source dir + launcher_file = "%s/exec__.js" % source_dir + with codecs.open(launcher_file, 'w', 'utf-8') as d: + with codecs.open(launcher, 'r', 'utf-8') as s: + body = s.read() + body = body.replace('require("./##MAIN_FILE##").##MAIN_FUNC##', 'require("./%s").%s' % (main_file, main_func)) + d.write(body) + +def build(source_dir, target_file): + with codecs.open(target_file, 'w', 'utf-8') as d: + d.write("""#!/bin/bash +cd %s +exec node exec__.js +""" % source_dir) + os.chmod(target_file, 0o755) + +def main(argv): + if len(argv) < 4: + print("usage: ") + sys.exit(1) + + main = argv[1] + source_dir = os.path.abspath(argv[2]) + target_file = os.path.abspath("%s/exec" % argv[3]) + launcher = "/nodejsAction/launcher.js" + sources(launcher, source_dir, main) + build(source_dir, target_file) + +if __name__ == '__main__': + main(sys.argv) diff --git a/core/typescript37Action/build.gradle b/core/nodejsActionBase/lambda.js similarity index 74% rename from core/typescript37Action/build.gradle rename to core/nodejsActionBase/lambda.js index 1a17b564..51ec6721 100644 --- a/core/typescript37Action/build.gradle +++ b/core/nodejsActionBase/lambda.js @@ -15,16 +15,4 @@ * limitations under the License. */ -ext.dockerImageName = 'action-typescript-v3.7' -apply from: '../../gradle/docker.gradle' - -distDocker.dependsOn 'copyPackageJson' - -task copyPackageJson(type: Copy) { - from '../nodejsActionBase/package.json' - into '.' -} - -task cleanup(type: Delete) { - delete 'package.json' -} +// this is a place holder file diff --git a/core/typescript37Action/lib/launcher.ts b/core/nodejsActionBase/launcher.js similarity index 53% rename from core/typescript37Action/lib/launcher.ts rename to core/nodejsActionBase/launcher.js index 95b428d9..d89af56b 100644 --- a/core/typescript37Action/lib/launcher.ts +++ b/core/nodejsActionBase/launcher.js @@ -15,31 +15,22 @@ * limitations under the License. */ +const useLambdaRunner = require('/nodejsAction/useLambdaRunner'); +const readline = require('readline'); +const fs = require("fs") + try { - const main = require("./main__").main - const readline = require('readline'); - const fs = require("fs") - const os = require("os") + const { NodeActionRunner } = require('/nodejsAction/runner'); + const NodeActionLambdaRunner = (() => { + try { + let lambda = require('/nodejsAction/lambda'); + return lambda; + } catch (e) {} + })(); - function vscodeDebug() { - let ifaces = os.networkInterfaces() - for (let iface of Object.keys(ifaces)) { - for (let ip of ifaces[iface]) { - if (!ip.internal) { - return { - "type": "node", - "request": "attach", - "name": process.env["__OW_ACTION_NAME"], - "address": ip.address, - "port": 8081, - "localRoot": "${workspaceFolder}", - "remoteRoot": __dirname - } - } - } - } - return { "error": "cannot find external interface" } - } + const handler = eval('require("./##MAIN_FILE##").##MAIN_FUNC##') // Will be replaced in the compile script with the correct main. + + const runner = useLambdaRunner(handler) ? new NodeActionLambdaRunner(handler) : new NodeActionRunner(handler); async function actionLoop() { const out = fs.createWriteStream(null, @@ -48,7 +39,6 @@ try { const rl = readline.createInterface({ input: process.stdin }); - const debugging = "__OW_DEBUG_PORT" in process.env out.write(JSON.stringify({ "ok": true }) + "\n"); for await (const line of rl) { try { @@ -60,41 +50,35 @@ try { process.env[envar] = args[key] } } - let result = {} - if (debugging && "debugWith" in value) { - if (value["debugWith"] === "vscode") - result = vscodeDebug() - else - result = { "error": "requested unknown debugger" } - } else { - result = main(value) - if (typeof result === 'undefined') { - result = {} - } - if (Promise.resolve(result) == result) - try { - result = await result - } catch (error) { - if (typeof error === 'undefined') { - error = {} - } - result = { "error": error } + let result = await runner.run(value).then(result => { + if (typeof result !== 'object') { + console.error(`Result must be of type object but has type "${typeof result}":`, result); } - } + writeMarkers(); + return result; + }); + out.write(JSON.stringify(result) + "\n"); } catch (err) { console.log(err); let message = err.message || err.toString() let error = { "error": message } + writeMarkers(); out.write(JSON.stringify(error) + "\n"); } } } actionLoop() } catch (e) { - if (e.code == "MODULE_NOT_FOUND") { - console.log("zipped actions must contain either package.json or index.js at the root.") - } console.log(e) process.exit(1) } + +// Create explicit stdout and stderr streams for the sentinels to avoid users tampering with the output. +const stdout = fs.createWriteStream(null, {fd: 1, encoding: "utf8"}) +const stderr = fs.createWriteStream(null, {fd: 2, encoding: "utf8"}) + +function writeMarkers() { + stdout.write('XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n'); + stderr.write('XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n'); +} diff --git a/tests/src/test/scala/runtime/actionContainers/NodeJs12ConcurrentTests.scala b/core/nodejsActionBase/nim.js similarity index 70% rename from tests/src/test/scala/runtime/actionContainers/NodeJs12ConcurrentTests.scala rename to core/nodejsActionBase/nim.js index e3414c40..51ec6721 100644 --- a/tests/src/test/scala/runtime/actionContainers/NodeJs12ConcurrentTests.scala +++ b/core/nodejsActionBase/nim.js @@ -15,13 +15,4 @@ * limitations under the License. */ -package runtime.actionContainers - -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class NodeJs12ConcurrentTests extends NodeJsConcurrentTests { - override lazy val nodejsContainerImageName = "action-nodejs-v12" - override lazy val nodejsTestDockerImageName = "nodejs12docker" -} +// this is a place holder file diff --git a/core/nodejsActionBase/package.json b/core/nodejsActionBase/package.json index 67cbe7b7..32799234 100644 --- a/core/nodejsActionBase/package.json +++ b/core/nodejsActionBase/package.json @@ -7,22 +7,27 @@ "url": "git@github.com:apache/openwhisk-runtime-nodejs.git" }, "license": "Apache-2.0", + "scripts": { + "test": "mocha" + }, "devDependencies": { "btoa": "1.1.2", + "chai": "^4.3.7", "eslint": "^5.16.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.17.2", "eslint-plugin-node": "^8.0.1", "eslint-plugin-promise": "^4.1.1", "eslint-plugin-standard": "^4.0.0", + "mocha": "^10.1.0", "request": "2.79.0" }, "dependencies": { - "openwhisk": "3.21.3", "body-parser": "1.18.3", "express": "4.16.4", - "serialize-error": "3.0.0", + "openwhisk": "3.21.3", "redis": "2.8.0", + "serialize-error": "3.0.0", "uuid": "3.3.0" } } diff --git a/core/nodejsActionBase/platform/knative.js b/core/nodejsActionBase/platform/knative.js deleted file mode 100644 index d0dbdb0b..00000000 --- a/core/nodejsActionBase/platform/knative.js +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -const OW_ENV_PREFIX = "__OW_"; -const CONTENT_TYPE = "Content-Type"; - -/** - * Determine if runtime is a "stem" cell, i.e., can be initialized with request init. data - * @param env - * @returns {boolean} - */ -function isStemCell(env) { - let actionCode = env.__OW_ACTION_CODE; - // It is a stem cell if valid code is "built into" the runtime's process environment. - return (typeof actionCode === 'undefined' || actionCode.length === 0); -} - -/** - * Determine if the request (body) contains valid activation data. - * @param req - * @returns {boolean} - */ -function hasActivationData(req) { - // it is a valid activation if the body contains an activation and value keys with data. - if (typeof req.body !== "undefined" && - typeof req.body.activation !== "undefined" && - typeof req.body.value !== "undefined") { - return true; - } - return false; -} - -/** - * Determine if the request (body) contains valid init data. - * @param req - * @returns {boolean} - */ -function hasInitData(req) { - // it is a valid init. if the body contains an init key with data. - if (typeof req.body !== "undefined" && - typeof req.body.init !== "undefined") { - return true; - } - return false; -} - - -/** - * Remove all INIT data from the value data that will be passed to the user function. - * @param body - */ -function removeInitData(body) { - - if (typeof body !== "undefined" && - typeof body.value !== "undefined") { - delete body.value.code; - delete body.value.main; - delete body.value.binary; - delete body.value.raw; - } -} - - -/** - * Create request init data from the process environment - */ -function createInitDataFromEnvironment(env) { - try { - var initdata = {}; - initdata.main = (typeof env.__OW_ACTION_MAIN === 'undefined') ? "main" : env.__OW_ACTION_MAIN; - // TODO: Throw error if CODE is NOT defined! - initdata.code = (typeof env.__OW_ACTION_CODE === 'undefined') ? "" : env.__OW_ACTION_CODE; - initdata.binary = (typeof env.__OW_ACTION_BINARY === 'undefined') ? false : env.__OW_ACTION_BINARY.toLowerCase() === "true"; - // TODO: default to empty? - initdata.actionName = (typeof env.__OW_ACTION_NAME === 'undefined') ? "" : env.__OW_ACTION_NAME; - initdata.raw = (typeof env.__OW_ACTION_RAW === 'undefined') ? false : env.__OW_ACTION_RAW.toLowerCase() === "true"; - - return initdata; - - } catch(e){ - console.error(e); - throw("Unable to process Initialization data: " + e.message); - } -} - - -/** - * Pre-process the init data from the request - */ -function preProcessInitData(initdata, valuedata, activationdata) { - try { - // Look for init data within the request (i.e., "stem cell" runtime, where code is injected by request) - if (typeof(initdata) !== "undefined") { - - if (initdata.main && typeof initdata.main === 'string') { - valuedata.main = initdata.main; - } - if (initdata.code && typeof initdata.code === 'string') { - valuedata.code = initdata.code; - } - if (initdata.binary) { - if (typeof initdata.binary === 'boolean') { - valuedata.binary = initdata.binary; - } else { - throw ("Invalid Init. data; expected boolean for key 'binary'."); - } - } - if (initdata.raw) { - if (typeof initdata.raw === 'boolean') { - valuedata.raw = initdata.raw; - } else { - throw ("Invalid Init. data; expected boolean for key 'raw'."); - } - } - - // Action name is a special case, as we have a key collision on "name" between init. data and request - // param. data (as they both appear within "body.value") so we must save it to its final location - // as the default Action name as part of the activation data - if (initdata.name && typeof initdata.name === 'string') { - if (typeof (activationdata) !== "undefined") { - if (typeof (activationdata.action_name) === "undefined" || - (typeof (activationdata.action_name) === "string" && - activationdata.action_name.length === 0)) { - activationdata.action_name = initdata.name; - } - } - } - } - - } catch(e){ - console.error(e); - throw("Unable to process Initialization data: " + e.message); - } -} - -/** - * Pre-process HTTP request information and make it available as parameter data to the action function - * by moving it to where the route handlers expect it to be (i.e., in the JSON value data map). - * - * See: https://github.com/apache/openwhisk/blob/master/docs/webactions.md#http-context - * - * HTTP Context - * ============ - * All web actions, when invoked, receives additional HTTP request details as parameters to the action - * input argument. These include: - * - * __ow_method (type: string): the HTTP method of the request. - * __ow_headers (type: map string to string): the request headers. - * __ow_path (type: string): the unmatched path of the request (matching stops after consuming the action extension). - * __ow_user (type: string): the namespace identifying the OpenWhisk authenticated subject. - * __ow_body (type: string): the request body entity, as a base64 encoded string when content is - * binary or JSON object/array, or plain string otherwise. - * __ow_query (type: string): the query parameters from the request as an unparsed string. - * - * TODO: - * A request may not override any of the named __ow_ parameters above; doing so will result in a - * failed request with status equal to 400 Bad Request. - */ -function preProcessHTTPContext(req, valueData) { - try { - if (valueData.raw) { - // __ow_body is a base64 encoded string when content is binary or JSON object/array, - // or plain string otherwise. - if (typeof req.body.value === "string" && req.body.value !== undefined) { - valueData.__ow_body = req.body.value; - } else { - // make value data available as __ow_body - const tmpBody = Object.assign({}, req.body.value); - // delete main, binary, raw, and code from the body before sending it as an action argument - removeInitData(tmpBody); - delete tmpBody.main; - delete tmpBody.code; - delete tmpBody.binary; - delete tmpBody.raw; - var bodyStr = JSON.stringify(tmpBody); - // note: we produce an empty map if there are no query parms/ - valueData.__ow_body = Buffer.from(bodyStr).toString("base64");; - } - valueData.__ow_query = req.query; - } - - var namespace = ""; - if (process.env[OW_ENV_PREFIX + "NAMESPACE"] !== undefined) { - namespace = process.env[OW_ENV_PREFIX + "NAMESPACE"]; - } - valueData.__ow_user = namespace; - valueData.__ow_method = req.method; - valueData.__ow_headers = req.headers; - valueData.__ow_path = ""; - } catch (e) { - console.error(e); - throw ("Unable to process HTTP Context: " + e.message) - } -} - -/** - * Pre-process the incoming http request data, moving it to where the - * route handlers expect it to be for an openwhisk runtime. - */ -function preProcessActivationData(env, activationdata) { - try { - // Note: we move the values here so that the "run()" handler does not have - // to move them again. - Object.keys(activationdata).forEach( - function (k) { - if (typeof activationdata[k] === 'string') { - var envVariable = OW_ENV_PREFIX + k.toUpperCase(); - process.env[envVariable] = activationdata[k]; - } - } - ); - } catch(e){ - console.error(e); - throw("Unable to process Activation data: " + e.message); - } -} - -/** - * Pre-process the incoming http request data, moving it to where the - * route handlers expect it to be for an openwhisk runtime. - */ -function preProcessRequest(req){ - try { - let env = process.env || {}; - - // Get or create valid references to the various data we might encounter - // in a request such as Init., Activation and function parameter data. - let body = req.body || {}; - let valueData = body.value || {}; - let initData = body.init || {}; - let activationData = body.activation || {}; - - // process initialization (i.e., "init") data - if (hasInitData(req)) { - preProcessInitData(initData, valueData, activationData); - } - - if(hasActivationData(req)) { - // process HTTP request header and body to make it available to function as parameter data - preProcessHTTPContext(req, valueData); - - // process per-activation (i.e, "run") data - preProcessActivationData(env, activationData); - } - - // Fix up pointers in case we had to allocate new maps - req.body = body; - req.body.value = valueData; - req.body.init = initData; - req.body.activation = activationData; - - } catch(e){ - console.error(e); - // TODO: test this error is handled properly and results in an HTTP error response - throw("Unable to process request data: " + e.message); - } -} - -function postProcessResponse(req, result, res) { - - var content_types = { - json: 'application/json', - html: 'text/html', - png: 'image/png', - svg: 'image/svg+xml', - }; - - // After getting the result back from an action, update the HTTP headers, - // status code, and body based on its result if it includes one or more of the - // following as top level JSON properties: headers, statusCode, body - let statusCode = result.code; - let headers = {}; - let body = result.response; - let contentTypeInHeader = false; - - // statusCode: default is 200 OK if body is not empty otherwise 204 No Content - if (result.response.statusCode !== undefined) { - statusCode = result.response.statusCode; - delete body['statusCode']; - } - - // the default content-type for an HTTP response is application/json - // this default are overwritten with the action specified headers - if (result.response.headers !== undefined) { - headers = result.response.headers; - delete body['headers']; - } - - // addressing content-type v/s Content-Type - // marking 'Content-Type' as standard inside header - if (headers.hasOwnProperty(CONTENT_TYPE.toLowerCase())) { - headers[CONTENT_TYPE] = headers[CONTENT_TYPE.toLowerCase()]; - delete headers[CONTENT_TYPE.toLowerCase()]; - } - - // If a content-type header is not declared in the action result’s headers, - // the body is interpreted as application/json for non-string values, - // and text/html otherwise. - if (!headers.hasOwnProperty(CONTENT_TYPE)) { - if (result.response.body !== undefined && typeof result.response.body == "string") { - headers[CONTENT_TYPE] = content_types.html; - } else { - headers[CONTENT_TYPE] = content_types.json; - } - } else { - contentTypeInHeader = true; - } - - - // body: a string which is either a plain text, JSON object, or a base64 encoded string for binary data (default is "") - // body is considered empty if it is null, "", or undefined - if (result.response.body !== undefined) { - body = result.response.body; - delete body['main']; - delete body['code']; - delete body['binary']; - } - - // When the content-type is defined, check if the response is binary data or - // plain text and decode the plain text using a base64 decoder whenever needed. - // Should the body fail to decoded correctly, return an error to the caller. - if (contentTypeInHeader && headers[CONTENT_TYPE].lastIndexOf("image", 0) === 0) { - if (typeof body === "string") { - body = Buffer.from(body, 'base64') - headers["Content-Transfer-Encoding"] = "binary"; - } - // TODO: throw an error if body can not be decoded - } - - - // statusCode: set it to 204 No Content if body is empty - if (statusCode === 200 && body === "") { - statusCode = 204; - } - - if (!headers.hasOwnProperty('Access-Control-Allow-Origin')) { - headers['Access-Control-Allow-Origin'] = '*'; - } - if (!headers.hasOwnProperty('Access-Control-Allow-Methods')) { - headers['Access-Control-Allow-Methods'] = 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH'; - } - // the header Access-Control-Request-Headers is echoed back as the header Access-Control-Allow-Headers if it is present in the HTTP request. - // Otherwise, a default value is generated. - if (!headers.hasOwnProperty['Access-Control-Allow-Headers']) { - headers['Access-Control-Allow-Headers'] = 'Authorization, Origin, X - Requested - With, Content - Type, Accept, User - Agent'; - if (typeof req.headers['Access-Control-Request-Headers'] !== "undefined") { - headers['Access-Control-Allow-Headers'] = req.headers['Access-Control-Request-Headers']; - } - } - - res.header(headers).status(statusCode).send(body); -} - -function PlatformKnativeImpl(platformFactory) { - - var http_method = { - get: 'GET', - post: 'POST', - put: 'PUT', - delete: 'DELETE', - options: 'OPTIONS', - }; - - const DEFAULT_METHOD = [ 'POST' ]; - - // Provide access to common runtime services - var service = platformFactory.service; - - // TODO: Should we use app.WrapEndpoint()? - this.run = function(req, res) { - - try { - - // Do not process requests with init. data if this is not a "stem" cell - if (hasInitData(req) && !isStemCell(process.env)) - throw ("Cannot initialize a runtime with a dedicated function."); - - // If this is a dedicated, uninitialized runtime, then copy INIT data from env. into the request - if( !isStemCell(process.env) && !service.initialized()){ - let body = req.body || {}; - body.init = createInitDataFromEnvironment(process.env); - } - - // Different pre-processing logic based upon request data needed due Promise behavior - if(hasInitData(req) && hasActivationData(req)){ - // Request has both Init and Run (activation) data - preProcessRequest(req); - // Invoke the OW "init" entrypoint - service.initCode(req).then(function () { - // delete any INIT data (e.g., code, raw, etc.) from the 'value' data before calling run(). - removeInitData(req.body); - // Invoke the OW "run" entrypoint - service.runCode(req).then(function (result) { - postProcessResponse(req, result, res) - }); - }).catch(function (error) { - console.error(error); - if (typeof error.code === "number" && typeof error.response !== "undefined") { - res.status(error.code).json(error.response); - } else { - console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error)); - res.status(500).json({ error: "Internal error during function execution." }); - } - }); - } else if(hasInitData(req)){ - // Request has ONLY Init data - preProcessRequest(req); - // Invoke the OW "init" entrypoint - service.initCode(req).then(function (result) { - res.status(result.code).send(result.response); - }).catch(function (error) { - console.error(error); - if (typeof error.code === "number" && typeof error.response !== "undefined") { - res.status(error.code).json(error.response); - } else { - console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error)); - res.status(500).json({ error: "Internal error during function execution." }); - } - }); - } else if(hasActivationData(req)){ - // Request has ONLY Run (activation) data - preProcessRequest(req); - // Invoke the OW "run" entrypoint - service.runCode(req).then(function (result) { - postProcessResponse(req, result, res) - }).catch(function (error) { - console.error(error); - if (typeof error.code === "number" && typeof error.response !== "undefined") { - res.status(error.code).json(error.response); - } else { - console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error)); - res.status(500).json({ error: "Internal error during function execution." }); - } - }); - } else { - preProcessRequest(req); - // Invoke the OW "run" entrypoint - service.runCode(req).then(function (result) { - postProcessResponse(req, result, res) - }).catch(function (error) { - console.error(error); - if (typeof error.code === "number" && typeof error.response !== "undefined") { - res.status(error.code).json(error.response); - } else { - console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error)); - res.status(500).json({ error: "Internal error during function execution." }); - } - }); - } - } catch (e) { - res.status(500).json({error: "internal error during request processing."}) - } - }; - - // TODO: the 'httpMethods' var should not alternatively store string and string[] types - this.registerHandlers = function(app, platform) { - var httpMethods = process.env.__OW_HTTP_METHODS; - // default to "[post]" HTTP method if not defined - if (typeof httpMethods === "undefined") { - console.error("__OW_HTTP_METHODS is undefined; defaulting to '[post]' ..."); - httpMethods = DEFAULT_METHOD; - } else { - if (httpMethods.startsWith('[') && httpMethods.endsWith(']')) { - httpMethods = httpMethods.substr(1, httpMethods.length); - httpMethods = httpMethods.substr(0, httpMethods.length -1); - httpMethods = httpMethods.split(','); - } - } - // default to "[post]" HTTP method if specified methods are not valid - if (!Array.isArray(httpMethods) || !Array.length) { - console.error("__OW_HTTP_METHODS is undefined; defaulting to '[post]' ..."); - httpMethods = DEFAULT_METHOD; - } - - httpMethods.forEach(function (method) { - switch (method.toUpperCase()) { - case http_method.get: - app.get('/', platform.run); - break; - case http_method.post: - app.post('/', platform.run); - break; - case http_method.put: - app.put('/', platform.run); - break; - case http_method.delete: - app.delete('/', platform.run); - break; - case http_method.options: - app.options('/', platform.run); - break; - default: - console.error("Environment variable '__OW_HTTP_METHODS' has an unrecognized value (" + method + ")."); - } - }); - }; -} - -module.exports = PlatformKnativeImpl; diff --git a/core/nodejsActionBase/platform/openwhisk.js b/core/nodejsActionBase/platform/openwhisk.js deleted file mode 100644 index eefed16a..00000000 --- a/core/nodejsActionBase/platform/openwhisk.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -function PlatformOpenWhiskImpl(platformFactory) { - // Provide access to common runtime services - var service = platformFactory.service; - - this.registerHandlers = function(app, platform) { - app.post('/init', platformFactory.wrapEndpoint(service.initCode)); - app.post('/run', platformFactory.wrapEndpoint(service.runCode)); - }; -} - -module.exports = PlatformOpenWhiskImpl; diff --git a/core/nodejsActionBase/platform/platform.js b/core/nodejsActionBase/platform/platform.js deleted file mode 100644 index 1196ccce..00000000 --- a/core/nodejsActionBase/platform/platform.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Runtime platform factory - * - * This module is a NodeJS compatible version of a factory that will - * produce an implementation module provides OpenWhisk Language - * Runtime functionality and is able to register endpoints/handlers - * allowing to host OpenWhisk Actions and process OpenWhisk Activations. - */ - - -// Export supported platform impls. -const PLATFORM_OPENWHISK = 'openwhisk'; -const PLATFORM_KNATIVE = 'knative'; - -const SUPPORTED_PLATFORMS = [ - PLATFORM_OPENWHISK, - PLATFORM_KNATIVE -]; - -module.exports = class PlatformFactory { - - /** - * Object constructor - * @param app NodeJS express application instance - * @param cfg Runtime configuration - * @@param svc Runtime services (default handlers) - */ - constructor (app, cfg, svc) { - this._app = app; - this._service = svc; - this._config = cfg; - } - - /** - * @returns {string[]} List of supported platforms by their string ID - */ - static get SUPPORTED_PLATFORMS() { - return SUPPORTED_PLATFORMS; - } - - static get PLATFORM_OPENWHISK() { - return PLATFORM_OPENWHISK; - } - - static get PLATFORM_KNATIVE() { - return PLATFORM_KNATIVE; - } - - get app(){ - return this._app; - } - - get service(){ - return this._service; - } - - get config(){ - return this._config; - } - - /** - * validate if a platform ID is a known, supported value - * @param id Platform Id - */ - static isSupportedPlatform(id) { - if (SUPPORTED_PLATFORMS.indexOf(id) > -1) { - return true; - } - return false; - } - - /** - * Instantiate a platform implementation - * @param id Platform ID - * @returns {PlatformImpl} Platform instance (interface), as best can be done with NodeJS - */ - createPlatformImpl(id) { - // Load the appropriate implementation module and return reference to it - switch (id.toLowerCase()) { - case PLATFORM_KNATIVE: - const knPlatformImpl = require('./knative.js'); - this._platformImpl = new knPlatformImpl(this); - break; - case PLATFORM_OPENWHISK: - const owPlatformImpl = require('./openwhisk.js'); - this._platformImpl = new owPlatformImpl(this); - break; - default: - console.error("Platform ID is not a known value (" + id + ")."); - } - return this._platformImpl; - } - - /** - * Wraps an endpoint written to return a Promise into an express endpoint, - * producing the appropriate HTTP response and closing it for all controllable - * failure modes. - * - * The expected signature for the promise value (both completed and failed) - * is { code: int, response: object }. - * - * @param ep a request=>promise function - * @returns an express endpoint handler - */ - wrapEndpoint(ep) { - return function (req, res) { - try { - ep(req).then(function (result) { - res.status(result.code).json(result.response); - }).catch(function (error) { - if (typeof error.code === "number" && typeof error.response !== "undefined") { - res.status(error.code).json(error.response); - } else { - console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error)); - res.status(500).json({ error: "Internal error." }); - } - }); - } catch (e) { - // This should not happen, as the contract for the endpoints is to - // never (externally) throw, and wrap failures in the promise instead, - // but, as they say, better safe than sorry. - console.error("[wrapEndpoint]", "exception caught", e.message); - res.status(500).json({ error: "Internal error (exception)." }); - } - } - } -}; diff --git a/core/nodejsActionBase/prelauncher.js b/core/nodejsActionBase/prelauncher.js new file mode 100755 index 00000000..4f9c0f0d --- /dev/null +++ b/core/nodejsActionBase/prelauncher.js @@ -0,0 +1,115 @@ +#!/usr/local/bin/node + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const useLambdaRunner = require('/nodejsAction/useLambdaRunner'); +const readline = require('readline'); +const fs = require("fs") + +try { + const { NodeActionRunner, initializeActionHandler } = require('/nodejsAction/runner'); + const NodeActionLambdaRunner = (() => { + try { + let lambda = require('/nodejsAction/lambda'); + return lambda; + } catch (e) {} + })(); + + /** + * Initializes the user's function. Expected to be called with first line from ActionLoop input. + * @param {{ env: Object }} message + * @returns {NodeActionRunner | NodeActionLambdaRunner} The runner to use with the function. + */ + function doInit(message) { + if (message.env && typeof message.env == 'object') { + Object.keys(message.env).forEach(k => { + let val = message.env[k]; + if (typeof val !== 'object' || val == null) { + process.env[k] = val ? val.toString() : ""; + } else { + process.env[k] = JSON.stringify(val); + } + }); + } + + return initializeActionHandler(message) + .then(handler => { + return useLambdaRunner(handler) ? new NodeActionLambdaRunner(handler) : new NodeActionRunner(handler); + }); + } + + async function actionLoop() { + let initialized = false + let runner = null + + const out = fs.createWriteStream(null,{fd: 3, encoding: "utf8"}) + process.stdin.setEncoding('utf8'); + const rl = readline.createInterface({input: process.stdin}); + out.write(JSON.stringify({ "ok": true }) + "\n"); + + for await (const line of rl) { + try { + let args = JSON.parse(line) + let value = args.value || {} + + if (!initialized) { + // The first value sent through is the actual init. + runner = await doInit(value) + initialized = true + out.write(JSON.stringify({ "ok": true }) + "\n"); + continue; + } + + for (let key in args) { + if (key !== "value") { + let envar = "__OW_" + key.toUpperCase() + process.env[envar] = args[key] + } + } + let result = await runner.run(value).then(result => { + if (typeof result !== 'object') { + console.error(`Result must be of type object but has type "${typeof result}":`, result); + } + writeMarkers(); + return result; + }); + + out.write(JSON.stringify(result) + "\n"); + } catch (err) { + console.log(err); + let message = err.message || err.toString() + let error = { "error": message } + writeMarkers(); + out.write(JSON.stringify(error) + "\n"); + } + } + } + actionLoop() +} catch (e) { + console.log(e) + process.exit(1) +} + +// Create explicit stdout and stderr streams for the sentinels to avoid users tampering with the output. +const stdout = fs.createWriteStream(null, {fd: 1, encoding: "utf8"}) +const stderr = fs.createWriteStream(null, {fd: 2, encoding: "utf8"}) + +function writeMarkers() { + stdout.write('XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n'); + stderr.write('XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n'); +} diff --git a/core/nodejsActionBase/runner.js b/core/nodejsActionBase/runner.js index 43f758b7..2c401843 100644 --- a/core/nodejsActionBase/runner.js +++ b/core/nodejsActionBase/runner.js @@ -23,7 +23,11 @@ const fs = require('fs'); const path = require('path'); -/** Initializes the handler for the user function. */ +/** + * Initializes the handler for the user function. + * @param {Object} message + * @returns {Promise} + */ function initializeActionHandler(message) { if (message.binary) { // The code is a base64-encoded zip file. @@ -42,31 +46,70 @@ function initializeActionHandler(message) { // Set the executable directory to the project dir. process.chdir(moduleDir); - if (index === undefined && !fs.existsSync('package.json') && !fs.existsSync('index.js')) { - return Promise.reject('Zipped actions must contain either package.json or index.js at the root.'); + const packageJsonExists = fs.existsSync('package.json') + const indexJSExists = fs.existsSync('index.js') + const indexMJSExists = fs.existsSync('index.mjs') + if (index === undefined && !packageJsonExists && !indexJSExists && !indexMJSExists) { + return Promise.reject('Zipped functions must contain either package.json or index.[m]js at the root.'); + } + + let mainFile + if (index !== undefined) { + // Backwards compat: We allow for main definitions like: `file.a.b.c` + // where we'd import the function a.b.c from `file.[m]js`. + if (fs.existsSync(index + '.js')) { + mainFile = index + '.js' + } else if (fs.existsSync(index + '.mjs')) { + mainFile = index + '.mjs' + } + } + if (!mainFile && packageJsonExists) { + // Infer the main file from package.json by default. + let package = JSON.parse(fs.readFileSync('package.json')); + mainFile = package.main + } + if (!mainFile && indexMJSExists) { + mainFile = 'index.mjs' + } + if (!mainFile) { + mainFile = 'index.js' } // The module to require. - let whatToRequire = index !== undefined ? path.join(moduleDir, index) : moduleDir; - let handler = eval('require("' + whatToRequire + '").' + main); - return assertMainIsFunction(handler, message.main); + let handler = eval('import("' + path.join(moduleDir, mainFile) + '").then(evaled => evaled.' + main + ')'); + return handler.then(func => assertMainIsFunction(func, message.main)) }) .catch(error => Promise.reject(error)); - } else try { - let handler = eval( - `(function(){ - ${message.code} - try { - return ${message.main} - } catch (e) { - if (e.name === 'ReferenceError') { - return module.exports.${message.main} || exports.${message.main} - } else throw e - } - })()`); - return assertMainIsFunction(handler, message.main); - } catch (e) { - return Promise.reject(e); + } else { + return new Promise((resolve) => { + // Throws on error and rejects the promise as a consequence. + // In the eval below, ${message.code} will template in the user's code. ${message.main} will template in the + // name of the main function. So the code ends up being evaluated, which has the effect of declaring a + // function, and then that function is returned. If an error is thrown + // while trying to return the function using the current scope, and the error is ReferenceError, we assume + // that the user provided code by exporting a module instead of putting a function at the top level of their + // file, and we return what they exported instead. + // Either way, the result of eval will be the user's main function, as a function (not as a string etc.). + let handler = eval( + `(function(){ + ${message.code} + try { + return ${message.main} + } catch (e) { + if (e.name === 'ReferenceError') { + return module.exports.${message.main} || exports.${message.main} + } else throw e + } + })()`) + resolve(handler) + }) + .then(func => assertMainIsFunction(func, message.main)) + .catch(_ => { + // Write file as ES modules need to be loaded from files. + fs.writeFileSync('index.mjs', message.code) + return eval('import("' + process.cwd() + '/index.mjs").then(evaled => evaled.' + message.main + ')'); + }) + .then(func => assertMainIsFunction(func, message.main)) } } @@ -77,9 +120,24 @@ class NodeActionRunner { } run(args) { + let deadline = Number(process.env['__OW_DEADLINE']); + let context = { + functionName: process.env['__OW_ACTION_NAME'], + functionVersion: process.env['__OW_ACTION_VERSION'], + activationId: process.env['__OW_ACTIVATION_ID'], + requestId: process.env['__OW_TRANSACTION_ID'], + deadline: deadline, + apiHost: process.env['__OW_API_HOST'], + apiKey: process.env['__OW_API_KEY'] || '', + namespace: process.env['__OW_NAMESPACE'], + getRemainingTimeInMillis: function() { + return deadline - new Date().getTime(); + } + } + return new Promise((resolve, reject) => { try { - var result = this.userScriptMain(args); + var result = this.userScriptMain(args, context); } catch (e) { reject(e); } @@ -125,14 +183,14 @@ function unzipInTmpDir(zipFileContents) { const zipFile = path.join(tmpDir, "action.zip"); fs.writeFile(zipFile, zipFileContents, "base64", err => { if (!err) resolve(zipFile); - else reject("There was an error reading the action archive."); + else reject("There was an error reading the function archive."); }); }); }).then(zipFile => { return exec(mkTempCmd).then(tmpDir => { return exec("unzip -qq " + zipFile + " -d " + tmpDir) .then(res => path.resolve(tmpDir)) - .catch(error => Promise.reject("There was an error uncompressing the action archive.")); + .catch(error => Promise.reject("There was an error uncompressing the function archive.")); }); }); } @@ -170,7 +228,7 @@ function assertMainIsFunction(handler, name) { if (typeof handler === 'function') { return Promise.resolve(handler); } else { - return Promise.reject("Action entrypoint '" + name + "' is not a function."); + return Promise.reject("Function entrypoint '" + name + "' is not a function."); } } diff --git a/core/nodejsActionBase/src/service.js b/core/nodejsActionBase/src/service.js deleted file mode 100644 index e7812102..00000000 --- a/core/nodejsActionBase/src/service.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const { initializeActionHandler, NodeActionRunner } = require('../runner'); - -function NodeActionService(config) { - - const Status = { - ready: 'ready', - starting: 'starting', - running: 'running', - stopped: 'stopped', - }; - - const ignoreRunStatus = config.allowConcurrent === undefined ? false : config.allowConcurrent.toLowerCase() === 'true'; - - let status = Status.ready; - let server = undefined; - let userCodeRunner = undefined; - - function setStatus(newStatus) { - if (status !== Status.stopped) { - status = newStatus; - } - } - - /** - * An ad-hoc format for the endpoints returning a Promise representing, - * eventually, an HTTP response. - * - * The promised values (whether successful or not) have the form: - * { code: int, response: object } - * - */ - function responseMessage(code, response) { - return { code: code, response: response }; - } - - function errorMessage(code, errorMsg) { - return responseMessage(code, { error: errorMsg }); - } - - /** - * Indicates if we have been initialized which is determined by if we have - * created a NodeActionRunner. - * @returns {boolean} - */ - this.initialized = function isInitialized(){ - return (typeof userCodeRunner !== 'undefined'); - }; - - /** - * Starts the server. - * - * @param app express app - */ - this.start = function start(app) { - server = app.listen(config.port, function() { - var host = server.address().address; - var port = server.address().port; - }); - - // This is required as http server will auto disconnect in 2 minutes, this to not auto disconnect at all - server.timeout = 0; - }; - - /** Returns a promise of a response to the /init invocation. - * - * req.body = { main: String, code: String, binary: Boolean } - */ - this.initCode = function initCode(req) { - if (status === Status.ready && userCodeRunner === undefined) { - setStatus(Status.starting); - - let body = req.body || {}; - let message = body.value || {}; - - if (message.main && message.code && typeof message.main === 'string' && typeof message.code === 'string') { - return doInit(message).then(_ => { - setStatus(Status.ready); - return responseMessage(200, { OK: true }); - }).catch(error => { - setStatus(Status.stopped); - let errStr = `Initialization has failed due to: ${error.stack ? String(error.stack) : error}`; - return Promise.reject(errorMessage(502, errStr)); - }); - } else { - setStatus(Status.ready); - let msg = 'Missing main/no code to execute.'; - return Promise.reject(errorMessage(403, msg)); - } - } else if (userCodeRunner !== undefined) { - let msg = 'Cannot initialize the action more than once.'; - console.error('Internal system error:', msg); - return Promise.reject(errorMessage(403, msg)); - } else { - let msg = `System not ready, status is ${status}.`; - console.error('Internal system error:', msg); - return Promise.reject(errorMessage(403, msg)); - } - }; - - /** - * Returns a promise of a response to the /exec invocation. - * Note that the promise is failed if and only if there was an unhandled error - * (the user code threw an exception, or our proxy had an internal error). - * Actions returning { error: ... } are modeled as a Promise successful resolution. - * - * req.body = { value: Object, meta { activationId : int } } - */ - this.runCode = function runCode(req) { - if (status === Status.ready && userCodeRunner !== undefined) { - if (!ignoreRunStatus) { - setStatus(Status.running); - } - - // these are defensive checks against the expected interface invariants - let msg = req && req.body || {}; - if (msg.value === null || msg.value === undefined) { - msg.value = {}; - } else if (typeof msg.value !== 'object') { - let errStr = `Internal system error: the argument must be a dictionary but has type '${typeof msg.value}'.`; - console.error('Internal system error:', errStr); - return Promise.reject(errorMessage(403, errStr)); - } - - return doRun(msg).then(result => { - if (!ignoreRunStatus) { - setStatus(Status.ready); - } - if (typeof result !== 'object') { - return errorMessage(502, 'The action did not return a dictionary.'); - } else { - return responseMessage(200, result); - } - }).catch(error => { - let msg = `An error has occurred: ${error}`; - setStatus(Status.stopped); - return Promise.reject(errorMessage(502, msg)); - }); - } else { - let msg = userCodeRunner ? `System not ready, status is ${status}.` : 'System not initialized.'; - console.error('Internal system error:', msg); - return Promise.reject(errorMessage(403, msg)); - } - }; - - function doInit(message) { - if (message.env && typeof message.env == 'object') { - Object.keys(message.env).forEach(k => { - let val = message.env[k]; - if (typeof val !== 'object' || val == null) { - process.env[k] = val ? val.toString() : ""; - } else { - process.env[k] = JSON.stringify(val); - } - }); - } - - return initializeActionHandler(message) - .then(handler => { - userCodeRunner = new NodeActionRunner(handler); - }) - // emit error to activation log then flush the logs as this is the end of the activation - .catch(error => { - console.error('Error during initialization:', error); - writeMarkers(); - return Promise.reject(error); - }); - } - - function doRun(msg) { - // Move per-activation keys to process env. vars with __OW_ (reserved) prefix - Object.keys(msg).forEach(k => { - if (typeof msg[k] === 'string' && k !== 'value') { - let envVariable = '__OW_' + k.toUpperCase(); - process.env[envVariable] = msg[k]; - } - }); - - return userCodeRunner - .run(msg.value) - .then(result => { - if (typeof result !== 'object') { - console.error(`Result must be of type object but has type "${typeof result}":`, result); - } - writeMarkers(); - return result; - }).catch(error => { - console.error(error); - writeMarkers(); - return Promise.reject(error); - }); - } - - function writeMarkers() { - console.log('XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX'); - console.error('XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX'); - } -} - -NodeActionService.getService = config => new NodeActionService(config); - -module.exports = NodeActionService; diff --git a/core/nodejsActionBase/test/useLambdaRunner.spec.js b/core/nodejsActionBase/test/useLambdaRunner.spec.js new file mode 100644 index 00000000..9f3fca1f --- /dev/null +++ b/core/nodejsActionBase/test/useLambdaRunner.spec.js @@ -0,0 +1,41 @@ +const expect = require('chai').expect; + +const useLambdaRunner = require('../useLambdaRunner'); + +const envVarName = '__OW_LAMBDA_COMPAT'; + +describe('useLambdaRunner()', function () { + beforeEach(function () { + delete process.env[envVarName]; + }); + + it(`returns true when env var ${envVarName} is set to "true"`, function () { + process.env[envVarName] = 'true'; + + expect(useLambdaRunner(undefined)).to.equal(true); + }); + + it(`returns false when env var ${envVarName} is not set and it is given a function with no parameters`, function () { + const fn = function() {}; + + expect(useLambdaRunner(fn)).to.equal(false); + }); + + it(`returns false when env var ${envVarName} is not set and it is given a function with one parameter`, function () { + const fn = function(a) {}; + + expect(useLambdaRunner(fn)).to.equal(false); + }); + + it(`returns false when env var ${envVarName} is not set and it is given a function with two parameters`, function () { + const fn = function(a, b) {}; + + expect(useLambdaRunner(fn)).to.equal(false); + }); + + it(`returns false when env var ${envVarName} and it is given a function with three parameters`, function () { + const fn = function(a, b, c) {}; + + expect(useLambdaRunner(fn)).to.equal(false); + }); +}); diff --git a/core/nodejsActionBase/useLambdaRunner.js b/core/nodejsActionBase/useLambdaRunner.js new file mode 100644 index 00000000..4b433566 --- /dev/null +++ b/core/nodejsActionBase/useLambdaRunner.js @@ -0,0 +1,18 @@ +const envVarName = '__OW_LAMBDA_COMPAT'; + +/** + * Decides, based on environment variables and, if needed, the function signature, where to use the Lambda runner + * instead of the OpenWhisk runner. + * @param {Function} fn The function. + * @returns {boolean} Whether the Lambda runner should be used to run the function. + */ +function useLambdaRunner(fn) { + // Backwards compat: Deem function to be Lambda if env var is provided. This can be removed when we remove the + // Lambda only variant of the Node.js runtime. At that point, we would only use the Lambda runner if we detect that + // the function signature is Lambda-like. + if (process.env[envVarName] !== undefined && process.env[envVarName].toLowerCase() === 'true') { + return true; + } +} + +module.exports = useLambdaRunner; diff --git a/core/typescript37Action/.dockerignore b/core/typescript37Action/.dockerignore deleted file mode 100644 index a1d03cb9..00000000 --- a/core/typescript37Action/.dockerignore +++ /dev/null @@ -1,13 +0,0 @@ -*.*~ -*.yaml -*.tmpl -*.gradle -.dockerignore -.project -.settings -build.xml -Dockerfile -logs -node_modules -package-lock.json -test.js diff --git a/core/typescript37Action/Dockerfile b/core/typescript37Action/Dockerfile deleted file mode 100644 index 29cc334d..00000000 --- a/core/typescript37Action/Dockerfile +++ /dev/null @@ -1,80 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# build go proxy from source -FROM golang:1.15 AS builder_source -ARG GO_PROXY_GITHUB_USER=apache -ARG GO_PROXY_GITHUB_BRANCH=master -RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} \ - https://github.com/${GO_PROXY_GITHUB_USER}/openwhisk-runtime-go /src ;\ - cd /src ; env GO111MODULE=on CGO_ENABLED=0 go build main/proxy.go && \ - mv proxy /bin/proxy - -# or build it from a release -FROM golang:1.15 AS builder_release -ARG GO_PROXY_RELEASE_VERSION=1.15@1.17.0 -RUN curl -sL \ - https://github.com/apache/openwhisk-runtime-go/archive/{$GO_PROXY_RELEASE_VERSION}.tar.gz\ - | tar xzf -\ - && cd openwhisk-runtime-go-*/main\ - && GO111MODULE=on go build -o /bin/proxy - -FROM node:12.1.0-stretch - -# select the builder to use -ARG GO_PROXY_BUILD_FROM=release - -ENV TYPESCRIPT_VERSION=3.7.4 -ENV OW_COMPILER=/bin/compile -ENV OW_LOG_INIT_ERROR=1 -ENV OW_WAIT_FOR_ACK=1 -ENV OW_EXECUTION_ENV=openwhisk/typescript3.7 - -# Initial update and some basics. -# -RUN apt-get update && apt-get install -y \ - imagemagick \ - graphicsmagick \ - unzip \ - && rm -rf /var/lib/apt/lists/* &&\ - mkdir -p /app/action - -WORKDIR /proxy -COPY --from=builder_source /bin/proxy /bin/proxy_source -COPY --from=builder_release /bin/proxy /bin/proxy_release -RUN mv /bin/proxy_${GO_PROXY_BUILD_FROM} /bin/proxy - -# Add sources and copy the package.json to root container, -# so npm packages from user functions take precendence. -# -WORKDIR /app -COPY bin/compile /bin/compile -COPY lib/launcher.ts /lib/launcher.ts -COPY package.json / - -# Customize runtime with additional packages. -# -RUN cd / && npm install -g \ - yarn \ - typescript@${TYPESCRIPT_VERSION} \ - && npm install --no-package-lock --production @types/node@13.13.5 \ - && npm install --no-package-lock --production \ - && npm cache clean --force - -EXPOSE 8080 - -ENTRYPOINT ["/bin/proxy"] diff --git a/core/typescript37Action/Makefile b/core/typescript37Action/Makefile deleted file mode 100644 index 373f831a..00000000 --- a/core/typescript37Action/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -PREFIX=whisk -IMG=action-typescript-v3.7 - -start: - docker run -p 8080:8080 -ti -v $(PWD):/mnt $(IMG) - -debug: - docker run -p 8080:8080 -p 8081:8081 -ti --entrypoint=/bin/bash \ - -v $(PWD):/mnt -e OW_COMPILER=/mnt/bin/compile $(IMG) - -build: - docker build . -t $(IMG) - docker tag $(IMG) whisk/$(IMG) - docker images | grep $(IMG) - -.PHONY: start debug build diff --git a/core/typescript37Action/bin/compile b/core/typescript37Action/bin/compile deleted file mode 100755 index 2635459c..00000000 --- a/core/typescript37Action/bin/compile +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const path = require("path") -const fs = require("fs") -const execFileSync = require('child_process').execFileSync; - -// write a file creating intermediate directories -function write_file(file, body, executable) { - fs.mkdirSync(path.dirname(file), { recursive: true }) - fs.writeFileSync(file, body) - if (executable) { - fs.chmodSync(file, 0755) - } -} - -// copy a file eventually replacing a substring -function copy_replace(src, dst, match, replacement) { - let body = fs.readFileSync(src, "utf-8") - if (match) { - body = body.replace(match, replacement) - } - write_file(dst, body) -} - -function deext(filename) { - const pos = filename.lastIndexOf(".") - filename = pos > -1 ? filename.substring(0, pos) : filename - return filename -} - -// resolve dependencies from package.json - return the main file -function dependencies(src_dir) { - const pkg_config = src_dir + "/package.json" - const node_modules = src_dir + "/node_modules" - if (fs.existsSync(pkg_config)) { - if (!fs.existsSync(node_modules)) { - execFileSync("yarn", [], { - "cwd": src_dir - }) - } - const config = JSON.parse(fs.readFileSync(pkg_config, "utf-8")) - if ("main" in config) { - return deext(config["main"]) - } - } - return "index" -} - -// assemble sources -function sources(launcher, main_file, main_func, src_dir) { - // init config - const src_config = src_dir + "/tsconfig.json" - const config = {} - if (fs.existsSync(src_config)) { - config = JSON.parse(fs.readFileSync(src_config, "utf-8")) - } - - if (!("files" in config)) { - config["files"] = [] - } - - if (!("compilerOptions" in config)) { - config["compilerOptions"] = {} - } - - config["compilerOptions"]["inlineSourceMap"] = true - - if ("sourceMap" in config["compilerOptions"]) { - delete config["compilerOptions"]["sourceMap"] - } - - if (!("outDir" in config["compilerOptions"])) { - config["compilerOptions"]["outDir"] = "." - } - - // copy main src file if any (and use it as main) - const src_file = src_dir + "/exec" - const tgt_file = src_dir + "/" + main_file + ".ts" - if (fs.existsSync(src_file) && !fs.existsSync(tgt_file)) { - const re = RegExp('(? ") - process.exit(1) - } - const launcher = path.dirname(path.dirname(process.argv[1])) + "/lib/launcher.ts" - const src_dir = path.resolve(process.argv[3]) - const bin_dir = path.resolve(process.argv[4]) - let main_func = process.argv[2] - let main_file = dependencies(src_dir) - const pieces = main_func.split(".") - if (pieces.length > 1) { - main_file = pieces.shift() - main_func = pieces.join(".") - } - sources(launcher, main_file, main_func, src_dir) - build(src_dir, bin_dir) -} - -if (require.main === module) { - compile() -} diff --git a/tests/src/test/scala/runtime/actionContainers/NodeJs10ConcurrentTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJs10ConcurrentTests.scala deleted file mode 100644 index ffb3108c..00000000 --- a/tests/src/test/scala/runtime/actionContainers/NodeJs10ConcurrentTests.scala +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package runtime.actionContainers - -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class NodeJs10ConcurrentTests extends NodeJsConcurrentTests { - override lazy val nodejsContainerImageName = "action-nodejs-v10" - override lazy val nodejsTestDockerImageName = "nodejs10docker" -} diff --git a/tests/src/test/scala/runtime/actionContainers/NodeJs12ActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJs18ActionContainerTests.scala similarity index 88% rename from tests/src/test/scala/runtime/actionContainers/NodeJs12ActionContainerTests.scala rename to tests/src/test/scala/runtime/actionContainers/NodeJs18ActionContainerTests.scala index efba5698..8271b60c 100644 --- a/tests/src/test/scala/runtime/actionContainers/NodeJs12ActionContainerTests.scala +++ b/tests/src/test/scala/runtime/actionContainers/NodeJs18ActionContainerTests.scala @@ -21,7 +21,7 @@ import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) -class NodeJs12ActionContainerTests extends NodeJsActionContainerTests { - override lazy val nodejsContainerImageName = "action-nodejs-v12" - override lazy val nodejsTestDockerImageName = "nodejs12docker" +class NodeJs18ActionContainerTests extends NodeJsActionContainerTests { + override lazy val nodejsContainerImageName = "action-nodejs-v18" + override lazy val nodejsTestDockerImageName = "nodejs18docker" } diff --git a/tests/src/test/scala/runtime/actionContainers/NodeJs10ActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJs22ActionContainerTests.scala similarity index 82% rename from tests/src/test/scala/runtime/actionContainers/NodeJs10ActionContainerTests.scala rename to tests/src/test/scala/runtime/actionContainers/NodeJs22ActionContainerTests.scala index a7bcc447..3e45f5f1 100644 --- a/tests/src/test/scala/runtime/actionContainers/NodeJs10ActionContainerTests.scala +++ b/tests/src/test/scala/runtime/actionContainers/NodeJs22ActionContainerTests.scala @@ -21,7 +21,7 @@ import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) -class NodeJs10ActionContainerTests extends NodeJsActionContainerTests { - override lazy val nodejsContainerImageName = "action-nodejs-v10" - override lazy val nodejsTestDockerImageName = "nodejs10docker" +class NodeJs22ActionContainerTests extends NodeJsActionContainerTests { + override lazy val nodejsContainerImageName = "action-nodejs-v22" + override lazy val nodejsTestDockerImageName = "nodejs22docker" } diff --git a/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala index bb781808..ad91d689 100644 --- a/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala +++ b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala @@ -18,12 +18,14 @@ package runtime.actionContainers import java.io.File +import java.time.Instant import common.WskActorSystem import actionContainers.{ActionContainer, BasicActionRunnerTests, ResourceHelpers} import actionContainers.ActionContainer.withContainer import actionContainers.ResourceHelpers.ZipBuilder import spray.json._ +import spray.json.DefaultJsonProtocol._ abstract class NodeJsActionContainerTests extends BasicActionRunnerTests with WskActorSystem { @@ -177,10 +179,6 @@ abstract class NodeJsActionContainerTests extends BasicActionRunnerTests with Ws initCode should be(200) val (runCode, runRes) = c.run(runPayload(JsObject())) - // actionloop proxy does not return a different error code when there is an error, - // because it communicates only through json - if (!isTypeScript) - runCode should not be (200) runRes shouldBe defined runRes.get.fields.get("error") shouldBe defined @@ -407,10 +405,7 @@ abstract class NodeJsActionContainerTests extends BasicActionRunnerTests with Ws val (runCode, out) = c.run(runPayload(JsObject())) - if (isTypeScript) - out.get.fields should contain key ("error") - else - runCode should not be (200) + out.get.fields should contain key ("error") } @@ -754,8 +749,7 @@ abstract class NodeJsActionContainerTests extends BasicActionRunnerTests with Ws checkStreams(out, err, { case (o, e) => - (o + e).toLowerCase should include regex ("error|exited") - (o + e).toLowerCase should include("zipped actions must contain either package.json or index.js at the root.") + (o + e).toLowerCase should include("zipped functions must contain either package.json or index.[m]js at the root.") }) } @@ -933,4 +927,210 @@ abstract class NodeJsActionContainerTests extends BasicActionRunnerTests with Ws runRes.get.fields("error").toString.toLowerCase should include("error: app error") } } + + it should "allow the user to tamper with console functions" in { + assume(!isTypeScript) + val (out, err) = withNodeJsContainer { c => + val code = """ + | function main() { + | const baseLog = console.log + | console.log = function(arg) { + | baseLog("foo " + arg) + | } + | } + """.stripMargin; + + val (initCode, _) = c.init(initPayload(code)) + initCode should be(200) + + val (runCode, runRes) = c.run(runPayload(JsObject())) + runCode should be(200) + + runRes shouldBe defined + } + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + it should "have openwhisk package available through an ES module import" in { + val (out, err) = withNodeJsContainer { c => + val code = + """ + | import ow from 'openwhisk'; + | export function main(args) { + | return { "result": true }; + | } + """.stripMargin + + val (initCode, _) = c.init(initPayload(code)) + initCode should be(200) + + val (runCode, out) = c.run(runPayload(JsObject())) + runCode should be(200) + } + } + + it should "support zip-encoded ES module actions without a package.json file" in { + val srcs = Seq( + Seq("index.mjs") -> + """ + | export function main(args) { + | var name = typeof args["name"] === "string" ? args["name"] : "stranger"; + | + | return { + | greeting: "Hello " + name + ", from an ES module action without a package.json." + | }; + | } + | + """.stripMargin) + + val code = ZipBuilder.mkBase64Zip(srcs) + + val (out, err) = withNodeJsContainer { c => + c.init(initPayload(code))._1 should be(200) + + val (runCode, runRes) = c.run(runPayload(JsObject())) + + runCode should be(200) + runRes.get.fields.get("greeting") shouldBe Some( + JsString("Hello stranger, from an ES module action without a package.json.")) + } + } + + it should "support zip-encoded ES module with main from file other than index.mjs" in { + val srcs = Seq( + Seq("other.mjs") -> + """ + | export function niam(args) { + | var name = typeof args["name"] === "string" ? args["name"] : "stranger"; + | + | return { + | greeting: "Hello " + name + ", from other.niam." + | }; + | } + """.stripMargin) + + val code = ZipBuilder.mkBase64Zip(srcs) + + val (out, err) = withNodeJsContainer { c => + c.init(initPayload(code, "other.niam"))._1 should be(200) + + val (runCode, runRes) = c.run(runPayload(JsObject())) + runCode should be(200) + runRes.get.fields.get("greeting") shouldBe Some(JsString("Hello stranger, from other.niam.")) + } + + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + it should "support zip-encoded ES module with multiple js files, enabled through package.json" in { + val srcs = Seq( + Seq("package.json") -> + """ + | { + | "name": "wskaction", + | "version": "1.0.0", + | "description": "An OpenWhisk action as an npm package.", + | "main": "foo.js", + | "type": "module", + | "author": "info@openwhisk.org", + | "license": "Apache-2.0" + | } + """.stripMargin, + Seq("foo.js") -> + """ + | import {hello} from './bar.js'; + | export function main(args) { + | var name = typeof args["name"] === "string" ? args["name"] : "stranger"; + | + | return { + | greeting: hello(name) + | }; + | } + """.stripMargin, + Seq("bar.js") -> + """ + | export function hello(name) { + | return "Hello " + name + ", from module." + | } + """.stripMargin) + + val code = ZipBuilder.mkBase64Zip(srcs) + + val (out, err) = withNodeJsContainer { c => + c.init(initPayload(code))._1 should be(200) + + val (runCode, runRes) = c.run(runPayload(JsObject())) + runCode should be(200) + runRes.get.fields.get("greeting") shouldBe Some(JsString("Hello stranger, from module.")) + } + + checkStreams(out, err, { + case (o, e) => + o shouldBe empty + e shouldBe empty + }) + } + + Map( + "prelaunched" -> Map.empty[String, String], + "non-prelaunched" -> Map("OW_INIT_IN_ACTIONLOOP" -> ""), + ).foreach { case (name, env) => + it should s"support a function with a lambda-like signature $name" in { + val (out, err) = withActionContainer(env + ("__OW_API_HOST" -> "testhost")) { c => + val code = + """ + | function main(event, context) { + | return { + | "remaining_time": context.getRemainingTimeInMillis(), + | "activation_id": context.activationId, + | "request_id": context.requestId, + | "function_name": context.functionName, + | "function_version": context.functionVersion, + | "api_host": context.apiHost, + | "api_key": context.apiKey, + | "namespace": context.namespace + | } + | } + """.stripMargin + + val (initCode, _) = c.init(initPayload(code)) + initCode should be(200) + + val (runCode, out) = c.run(runPayload( + JsObject(), + Some(JsObject( + "deadline" -> Instant.now.plusSeconds(10).toEpochMilli.toString.toJson, + "activation_id" -> "testaid".toJson, + "transaction_id" -> "testtid".toJson, + "action_name" -> "testfunction".toJson, + "action_version" -> "0.0.1".toJson, + "namespace" -> "testnamespace".toJson, + "api_key" -> "testkey".toJson + )) + )) + runCode should be(200) + + val remainingTime = out.get.fields("remaining_time").convertTo[Int] + remainingTime should be > 9500 // We give the test 500ms of slack to invoke the function to avoid flakes. + out shouldBe Some(JsObject( + "remaining_time" -> remainingTime.toJson, + "activation_id" -> "testaid".toJson, + "request_id" -> "testtid".toJson, + "function_name" -> "testfunction".toJson, + "function_version" -> "0.0.1".toJson, + "api_host" -> "testhost".toJson, + "api_key" -> "testkey".toJson, + "namespace" -> "testnamespace".toJson + )) + } + } + } } diff --git a/tests/src/test/scala/runtime/actionContainers/Typescript37BasicTests.scala b/tests/src/test/scala/runtime/actionContainers/Typescript37BasicTests.scala deleted file mode 100644 index 4006cdec..00000000 --- a/tests/src/test/scala/runtime/actionContainers/Typescript37BasicTests.scala +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package runtime.actionContainers - -import actionContainers.ActionContainer.withContainer -import actionContainers.{ActionContainer, BasicActionRunnerTests} -import common.WskActorSystem -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class Typescript37BasicTests extends BasicActionRunnerTests with WskActorSystem { - - val image = "action-typescript-v3.7" - - override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = { - withContainer(image, env)(code) - } - - def withActionLoopContainer(code: ActionContainer => Unit) = - withContainer(image)(code) - - behavior of image - - override val testNoSourceOrExec = TestConfig("") - - override val testNotReturningJson = - TestConfig(""" - |export function main(args) { - | return "not a json object" - |} - """.stripMargin) - - override val testEcho = TestConfig("""|export function main(args) { - | console.log("hello stdout") - | console.error("hello stderr") - | return args - |} - """.stripMargin) - - override val testUnicode = TestConfig("""|export function main(args) { - | let delimiter = args['delimiter'] - | let msg = delimiter+" ☃ "+delimiter - | console.log(msg) - | return { "winter": msg } - |} - """.stripMargin) - - override val testEnv = TestConfig("""|export function main(args) { - | let env = process.env - | return { - | "api_host": env["__OW_API_HOST"], - | "api_key": env["__OW_API_KEY"], - | "namespace": env["__OW_NAMESPACE"], - | "activation_id": env["__OW_ACTIVATION_ID"], - | "action_name": env["__OW_ACTION_NAME"], - | "deadline": env["__OW_DEADLINE"], - | "action_version": env["__OW_ACTION_VERSION"] - | } - |} - """.stripMargin) - - override val testInitCannotBeCalledMoreThanOnce = TestConfig(s"""|export function main(args) { - | return args - |} - """.stripMargin) - - override val testEntryPointOtherThanMain = TestConfig( - s"""|export function niam(args) { - | return args - |} - """.stripMargin, - main = "niam") - - override val testLargeInput = TestConfig(s"""|export function main(args) { - | return args - |} - """.stripMargin) -} diff --git a/tests/src/test/scala/runtime/actionContainers/Typescript37CommonTests.scala b/tests/src/test/scala/runtime/actionContainers/Typescript37CommonTests.scala deleted file mode 100644 index e39e2463..00000000 --- a/tests/src/test/scala/runtime/actionContainers/Typescript37CommonTests.scala +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package runtime.actionContainers - -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class Typescript37CommonTests extends NodeJsNonConcurrentTests { - - override lazy val nodejsContainerImageName = "action-typescript-v3.7" - override lazy val nodejsTestDockerImageName = "typescript37docker" - override val isTypeScript = true -}