Swift playground action feature with dynamic library (#23)
* import form swift3action from OPenWhisk for local testing
* test swift snippets for playground REPL
* modify Package.swift to build dynamic libraries - currently only for
SwiftyJSON as that is the only one without issues ….
* build and run docker image with REPL playground inside
* tests and current status
* how output in README
* added license to test files
diff --git a/swift-playground-action/Dockerfile b/swift-playground-action/Dockerfile
new file mode 100644
index 0000000..2133673
--- /dev/null
+++ b/swift-playground-action/Dockerfile
@@ -0,0 +1,16 @@
+FROM swift3action:base
+
+ENV DEBIAN_FRONTEND noninteractive
+
+ENV PYTHONIOENCODING utf-8
+
+RUN sudo apt-get -y install libpython2.7
+
+COPY add_dylib.py /
+
+RUN /add_dylib.py
+
+WORKDIR /swift3Action/spm-build/
+RUN /bin/bash -c "/usr/bin/swift build"
+
+CMD ["/bin/bash", "-c", "cd /swift3Action && PYTHONIOENCODING='utf-8' python -u swift3runner.py"]
diff --git a/swift-playground-action/README.md b/swift-playground-action/README.md
new file mode 100644
index 0000000..2d7d84e
--- /dev/null
+++ b/swift-playground-action/README.md
@@ -0,0 +1,77 @@
+# Enabling OpenWhisk Swift3 Action to use REPL PLayground
+
+## Test
+
+```
+./build.sh
+./run.sh
+```
+
+Then inside container run basic test
+
+```
+time /usr/bin/swift /share/test0.swift
+Hello World
+```
+
+
+and using dynamic library inside REPL calling SwiftyJSON:
+
+```
+time swift -I/swift3Action/spm-build/.build/debug -L /swift3Action/spm-build/.build/debug -lSwiftyJSON /share/test1.swift
+Hello Joe Doe
+```
+
+## Current issues - unable to build dynamic version of OpenSSL library
+
+Unable to build OpenSSL - to reproduce install editor
+
+```
+apt-get -y install nano emacs
+```
+
+Modify Package.swift
+
+```
+cd /swift3Action/spm-build/
+emacs Packages/OpenSSL-0.2.2/Package.swift
+```
+
+to add
+
+```
+products.append(Product(name: "OpenSSL", type: .Library(.Dynamic), modules: "openssl"))
+```
+
+and then run
+
+```
+root@87f331718d1e:/swift3Action/spm-build# swift build
+error: the product named OpenSSL references a module that could not be found: openssl
+fix: reference only valid modules from the product
+```
+
+
+Using
+
+```
+products.append(Product(name: "OpenSSL", type: .Library(.Dynamic), modules: "OpenSSL"))
+```
+
+leads to
+
+```
+root@87f331718d1e:/swift3Action/spm-build# swift build
+Linking ./.build/debug/libOpenSSL.so
+<unknown>:0: error: no input files
+<unknown>:0: error: build had 1 command failures
+error: exit(1): /usr/bin/swift-build-tool -f /swift3Action/spm-build/.build/debug.yaml
+```
+
+
+Optimally this should work ...
+
+```
+time swift -I/swift3Action/spm-build/.build/debug -L /swift3Action/spm-build/.build/debug -lSwiftyJSON -lOpenSSL /share/test2.swift
+<unknown>:0: error: missing required module 'OpenSSL'
+```
diff --git a/swift-playground-action/add_dylib.py b/swift-playground-action/add_dylib.py
new file mode 100755
index 0000000..2f27b50
--- /dev/null
+++ b/swift-playground-action/add_dylib.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+#
+# Copyright 2015-2016 IBM Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+import os
+import re
+import sys
+import json
+
+#
+# find all paclages
+# add dylib statements cusotmized ot package name
+# rebuild all packages
+
+def procesPackageFile(packageName, path):
+ stmt = '\n\nproducts.append(Product(name: "{packageName}", type: .Library(.Dynamic), modules: "{packageName}"))'.format(packageName=packageName)
+ # only patch select packages
+ if packageName == 'SwiftyJSON' :
+ print(packageName, path, stmt)
+ with open(path, "a") as myfile:
+ myfile.write(stmt + '\n')
+
+
+
+def main():
+ print("hello friend")
+ for root, dirs, files in os.walk("/swift3Action/spm-build/Packages/"):
+ for f in files:
+ if f.endswith("Package.swift"):
+ fullPath = os.path.join(root, f)
+ parentDir = os.path.split(fullPath)[0]
+ baseDir = os.path.basename(parentDir)
+ # remove version from SwiftyJSON-14.2.0
+ m = re.match("(.*)-[\d+\.]+", baseDir)
+ packageName = m.group(1)
+ #print("baseDir="+baseDir+" packageName="+packageName)
+ procesPackageFile(packageName, fullPath)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/swift-playground-action/build.sh b/swift-playground-action/build.sh
new file mode 100755
index 0000000..9fa87e1
--- /dev/null
+++ b/swift-playground-action/build.sh
@@ -0,0 +1,5 @@
+# build docker images
+echo Building base image from OpenWhisk Swift3 Action
+(cd swift3Action; docker build -t swift3action:base .)
+echo Building Swift Playground enabled version of OpenWhisk Swift3 Action
+docker build -t swift3action:playground .
diff --git a/swift-playground-action/run.sh b/swift-playground-action/run.sh
new file mode 100755
index 0000000..beb65d3
--- /dev/null
+++ b/swift-playground-action/run.sh
@@ -0,0 +1,6 @@
+
+
+# Need to use --privileged=true or REPL fails see:
+# https://bugs.swift.org/browse/SR-54
+
+docker run --privileged=true -it -v $(pwd):/share swift3action:playground /bin/bash
diff --git a/swift-playground-action/swift3Action/Dockerfile b/swift-playground-action/swift3Action/Dockerfile
new file mode 100644
index 0000000..2f2fb4b
--- /dev/null
+++ b/swift-playground-action/swift3Action/Dockerfile
@@ -0,0 +1,68 @@
+# Dockerfile for swift actions, overrides and extends ActionRunner from actionProxy
+# This Dockerfile is partially based on: https://github.com/swiftdocker/docker-swift/
+FROM buildpack-deps:trusty
+
+ENV DEBIAN_FRONTEND noninteractive
+
+# Upgrade and install basic Python dependencies
+RUN apt-get -y purge \
+ && apt-get -y update \
+ && apt-get -y install --fix-missing python2.7 python-gevent python-flask \
+\
+# Upgrade and install Swift dependencies
+ && apt-get -y install --fix-missing build-essential curl wget libicu-dev \
+\
+# Install zip for compiling Swift actions
+ && apt-get -y install zip \
+\
+# Clean up
+ && apt-get clean
+
+# Install clang manually, since SPM wants at least Clang 3-6
+RUN cd / &&\
+(curl -L -k http://llvm.org/releases/3.6.2/clang+llvm-3.6.2-x86_64-linux-gnu-ubuntu-14.04.tar.xz | tar xJ) &&\
+cp -r /clang+llvm-3.6.2-x86_64-linux-gnu-ubuntu-14.04/* /usr/ &&\
+rm -rf /clang+llvm-3.6.2-x86_64-linux-gnu-ubuntu-14.04
+
+RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/clang++ 20
+RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/clang 20
+
+# Install Swift keys
+RUN wget --no-verbose -O - https://swift.org/keys/all-keys.asc | gpg --import - && \
+ gpg --keyserver hkp://pool.sks-keyservers.net --refresh-keys Swift
+
+# Install Swift Ubuntu 14.04 Snapshot
+#https://swift.org/builds/swift-3.0.1-release/ubuntu1404/swift-3.0.1-RELEASE/swift-3.0.1-RELEASE-ubuntu14.04.tar.gz
+
+ENV SWIFT_VERSION 3.0.2
+ENV SWIFT_RELEASE_TYPE RELEASE
+ENV SWIFT_PLATFORM ubuntu14.04
+
+RUN SWIFT_ARCHIVE_NAME=swift-$SWIFT_VERSION-$SWIFT_RELEASE_TYPE-$SWIFT_PLATFORM && \
+ SWIFT_URL=https://swift.org/builds/swift-$SWIFT_VERSION-$(echo "$SWIFT_RELEASE_TYPE" | tr '[:upper:]' '[:lower:]')/$(echo "$SWIFT_PLATFORM" | tr -d .)/swift-$SWIFT_VERSION-$SWIFT_RELEASE_TYPE/$SWIFT_ARCHIVE_NAME.tar.gz && \
+ echo $SWIFT_URL && \
+ wget --no-verbose $SWIFT_URL && \
+ wget --no-verbose $SWIFT_URL.sig && \
+ gpg --verify $SWIFT_ARCHIVE_NAME.tar.gz.sig && \
+ tar -xzf $SWIFT_ARCHIVE_NAME.tar.gz --directory / --strip-components=1 && \
+ rm -rf $SWIFT_ARCHIVE_NAME* /tmp/* /var/tmp/*
+
+# Add the action proxy
+RUN mkdir -p /actionProxy
+ADD actionproxy.py /actionProxy
+
+# Add files needed to build and run action
+RUN mkdir -p /swift3Action
+ADD epilogue.swift /swift3Action
+ADD buildandrecord.py /swift3Action
+ADD swift3runner.py /swift3Action
+ADD spm-build /swift3Action/spm-build
+
+
+# Build kitura net
+RUN touch /swift3Action/spm-build/main.swift
+RUN python /swift3Action/buildandrecord.py; rm /swift3Action/spm-build/.build/release/Action
+#RUN cd /swift3Action/spm-build; swift build -c release; rm /swift3Action/spm-build/.build/release/Action
+ENV FLASK_PROXY_PORT 8080
+
+CMD ["/bin/bash", "-c", "cd /swift3Action && PYTHONIOENCODING='utf-8' python -u swift3runner.py"]
diff --git a/swift-playground-action/swift3Action/README.md b/swift-playground-action/swift3Action/README.md
new file mode 100644
index 0000000..e5e454d
--- /dev/null
+++ b/swift-playground-action/swift3Action/README.md
@@ -0,0 +1,37 @@
+Skeleton for "docker actions"
+================
+
+The `dockerskeleton` base image is useful for actions that run scripts (e.g., bash, perl, python)
+and compiled binaries or, more generally, any native executable. It provides a proxy service
+(using Flask, a Python web microframework) that implements the required `/init` and `/run` routes
+to interact with the OpenWhisk invoker service. The implementation of these routes is encapsulated
+in a class named `ActionRunner` which provides a basic framework for receiving code from an invoker,
+preparing it for execution, and then running the code when required.
+
+The initialization of the `ActionRunner` is done via `init()` which receives a JSON object containing
+a `code` property whose value is the source code to execute. It writes the source to a `source` file.
+This method also provides a hook to optionally augment the received code via an `epilogue()` method,
+and then performs a `build()` to generate an executable. The last step of the initialization applies
+`verify()` to confirm the executable has the proper permissions to run the code. The action runner
+is ready to run the action if `verify()` is true.
+
+The default implementations of `epilogue()` and `build()` are no-ops and should be overridden as needed.
+The base image contains a stub added which is already executable by construction via `docker build`.
+For language runtimes (e.g., C) that require compiling the source, the extending class should run the
+required source compiler during `build()`.
+
+The `run()` method runs the action via the executable generated during `init()`. This method is only called
+by the proxy service if `verify()` is true. `ActionRunner` subclasses are encouraged to override this method
+if they have additional logic that should cause `run()` to never execute. The `run()` method calls the executable
+via a process and sends the received input parameters (from the invoker) to the action via the command line
+(as a JSON string argument). Additional properties received from the invoker are passed on to the action via
+environment variables as well. To augment the action environment, override `env()`.
+
+By convention the action executable may log messages to `stdout` and `stderr`. The proxy requires that the last
+line of output to `stdout` is a valid JSON object serialized to string if the action returns a JSON result.
+A return value is optional but must be a JSON object (properly serialized) if present.
+
+For an example implementation of an `ActionRunner` that overrides `epilogue()` and `build()` see the
+[Swift 3](../swift3Action/swift3runner.py) action proxy. An implementation of the runner for Python actions
+is available [here](../pythonAction/pythonrunner.py). Lastly, an example Docker action that uses `C` is
+available in this [example](../../sdk/docker/Dockerfile).
diff --git a/swift-playground-action/swift3Action/actionproxy.py b/swift-playground-action/swift3Action/actionproxy.py
new file mode 100644
index 0000000..c68231f
--- /dev/null
+++ b/swift-playground-action/swift3Action/actionproxy.py
@@ -0,0 +1,271 @@
+"""Executable Python script for a proxy service to dockerSkeleton.
+
+Provides a proxy service (using Flask, a Python web microframework)
+that implements the required /init and /run routes to interact with
+the OpenWhisk invoker service.
+
+The implementation of these routes is encapsulated in a class named
+ActionRunner which provides a basic framework for receiving code
+from an invoker, preparing it for execution, and then running the
+code when required.
+
+/*
+ * 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.
+ */
+"""
+
+import sys
+import os
+import json
+import subprocess
+import codecs
+import flask
+from gevent.wsgi import WSGIServer
+import zipfile
+import io
+import base64
+
+
+class ActionRunner:
+ """ActionRunner."""
+ LOG_SENTINEL = 'XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX'
+
+ # initializes the runner
+ # @param source the path where the source code will be located (if any)
+ # @param binary the path where the binary will be located (may be the
+ # same as source code path)
+ def __init__(self, source=None, binary=None):
+ defaultBinary = '/action/exec'
+ self.source = source if source else defaultBinary
+ self.binary = binary if binary else defaultBinary
+
+ def preinit(self):
+ return
+
+ # extracts from the JSON object message a 'code' property and
+ # writes it to the <source> path. The source code may have an
+ # an optional <epilogue>. The source code is subsequently built
+ # to produce the <binary> that is executed during <run>.
+ # @param message is a JSON object, should contain 'code'
+ # @return True iff binary exists and is executable
+ def init(self, message):
+ def prep():
+ self.preinit()
+ if 'code' in message and message['code'] is not None:
+ binary = message['binary'] if 'binary' in message else False
+ if not binary:
+ return self.initCodeFromString(message)
+ else:
+ return self.initCodeFromZip(message)
+ else:
+ return False
+
+ if prep():
+ try:
+ # write source epilogue if any
+ # the message is passed along as it may contain other
+ # fields relevant to a specific container.
+ if self.epilogue(message) is False:
+ return False
+ # build the source
+ if self.build(message) is False:
+ return False
+ except Exception:
+ return False
+ # verify the binary exists and is executable
+ return self.verify()
+
+ # optionally appends source to the loaded code during <init>
+ def epilogue(self, init_arguments):
+ return
+
+ # optionally builds the source code loaded during <init> into an executable
+ def build(self, init_arguments):
+ return
+
+ # @return True iff binary exists and is executable, False otherwise
+ def verify(self):
+ return (os.path.isfile(self.binary) and
+ os.access(self.binary, os.X_OK))
+
+ # constructs an environment for the action to run in
+ # @param message is a JSON object received from invoker (should
+ # contain 'value' and 'api_key' and other metadata)
+ # @return an environment dictionary for the action process
+ def env(self, message):
+ # make sure to include all the env vars passed in by the invoker
+ env = os.environ
+ for p in ['api_key', 'namespace', 'action_name', 'activation_id', 'deadline']:
+ if p in message:
+ env['__OW_%s' % p.upper()] = message[p]
+ return env
+
+ # runs the action, called iff self.verify() is True.
+ # @param args is a JSON object representing the input to the action
+ # @param env is the environment for the action to run in (defined edge
+ # host, auth key)
+ # return JSON object result of running the action or an error dictionary
+ # if action failed
+ def run(self, args, env):
+ def error(msg):
+ # fall through (exception and else case are handled the same way)
+ sys.stdout.write('%s\n' % msg)
+ return (502, {'error': 'The action did not return a dictionary.'})
+
+ try:
+ input = json.dumps(args)
+ p = subprocess.Popen(
+ [self.binary, input],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env)
+ except Exception as e:
+ return error(e)
+
+ # run the process and wait until it completes.
+ # stdout/stderr will always be set because we passed PIPEs to Popen
+ (o, e) = p.communicate()
+
+ # stdout/stderr may be either text or bytes, depending on Python
+ # version, so if bytes, decode to text. Note that in Python 2
+ # a string will match both types; so also skip decoding in that case
+ if isinstance(o, bytes) and not isinstance(o, str):
+ o = o.decode('utf-8')
+ if isinstance(e, bytes) and not isinstance(e, str):
+ e = e.decode('utf-8')
+
+ # get the last line of stdout, even if empty
+ lastNewLine = o.rfind('\n', 0, len(o)-1)
+ if lastNewLine != -1:
+ # this is the result string to JSON parse
+ lastLine = o[lastNewLine+1:].strip()
+ # emit the rest as logs to stdout (including last new line)
+ sys.stdout.write(o[:lastNewLine+1])
+ else:
+ # either o is empty or it is the result string
+ lastLine = o.strip()
+
+ if e:
+ sys.stderr.write(e)
+
+ try:
+ json_output = json.loads(lastLine)
+ if isinstance(json_output, dict):
+ return (200, json_output)
+ else:
+ return error(lastLine)
+ except Exception:
+ return error(lastLine)
+
+ # initialize code from inlined string
+ def initCodeFromString(self, message):
+ with codecs.open(self.source, 'w', 'utf-8') as fp:
+ fp.write(message['code'])
+ return True
+
+ # initialize code from base64 encoded archive
+ def initCodeFromZip(self, message):
+ try:
+ bytes = base64.b64decode(message['code'])
+ bytes = io.BytesIO(bytes)
+ archive = zipfile.ZipFile(bytes)
+ archive.extractall(os.path.dirname(self.source))
+ archive.close()
+ return True
+ except Exception as e:
+ print('err', str(e))
+ return False
+
+proxy = flask.Flask(__name__)
+proxy.debug = False
+runner = None
+
+
+def setRunner(r):
+ global runner
+ runner = r
+
+
+@proxy.route('/init', methods=['POST'])
+def init():
+ message = flask.request.get_json(force=True, silent=True)
+ if message and not isinstance(message, dict):
+ flask.abort(404)
+ else:
+ value = message.get('value', {}) if message else {}
+
+ if not isinstance(value, dict):
+ flask.abort(404)
+
+ try:
+ status = runner.init(value)
+ except Exception as e:
+ status = False
+
+ if status is True:
+ return ('OK', 200)
+ else:
+ response = flask.jsonify({'error': 'The action failed to generate or locate a binary. See logs for details.'})
+ response.status_code = 502
+ return complete(response)
+
+
+@proxy.route('/run', methods=['POST'])
+def run():
+ def error():
+ response = flask.jsonify({'error': 'The action did not receive a dictionary as an argument.'})
+ response.status_code = 404
+ return complete(response)
+
+ message = flask.request.get_json(force=True, silent=True)
+ if message and not isinstance(message, dict):
+ return error()
+ else:
+ args = message.get('value', {}) if message else {}
+ if not isinstance(args, dict):
+ return error()
+
+ if runner.verify():
+ try:
+ code, result = runner.run(args, runner.env(message or {}))
+ response = flask.jsonify(result)
+ response.status_code = code
+ except Exception as e:
+ response = flask.jsonify({'error': 'Internal error. {}'.format(e)})
+ response.status_code = 500
+ else:
+ response = flask.jsonify({'error': 'The action failed to locate a binary. See logs for details.'})
+ response.status_code = 502
+ return complete(response)
+
+
+def complete(response):
+ # Add sentinel to stdout/stderr
+ sys.stdout.write('%s\n' % ActionRunner.LOG_SENTINEL)
+ sys.stdout.flush()
+ sys.stderr.write('%s\n' % ActionRunner.LOG_SENTINEL)
+ sys.stderr.flush()
+ return response
+
+
+def main():
+ port = int(os.getenv('FLASK_PROXY_PORT', 8080))
+ server = WSGIServer(('', port), proxy, log=None)
+ server.serve_forever()
+
+if __name__ == '__main__':
+ setRunner(ActionRunner())
+ main()
diff --git a/swift-playground-action/swift3Action/build.gradle b/swift-playground-action/swift3Action/build.gradle
new file mode 100644
index 0000000..0148e31
--- /dev/null
+++ b/swift-playground-action/swift3Action/build.gradle
@@ -0,0 +1,13 @@
+ext.dockerImageName = 'swift3action'
+apply from: '../../gradle/docker.gradle'
+distDocker.dependsOn 'copyProxy'
+distDocker.finalizedBy('rmProxy')
+
+task copyProxy(type: Copy) {
+ from '../actionProxy/actionproxy.py'
+ into './actionproxy.py'
+}
+
+task rmProxy(type: Delete) {
+ delete 'actionproxy.py'
+}
diff --git a/swift-playground-action/swift3Action/buildandrecord.py b/swift-playground-action/swift3Action/buildandrecord.py
new file mode 100644
index 0000000..c83c024
--- /dev/null
+++ b/swift-playground-action/swift3Action/buildandrecord.py
@@ -0,0 +1,76 @@
+"""Python to generate build script.
+
+/*
+ * 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
+from subprocess import check_output
+
+# Settings
+COMPILE_PREFIX = "/usr/bin/swiftc -module-name Action "
+LINKER_PREFIX = "/usr/bin/swiftc -Xlinker '-rpath=$ORIGIN' " \
+ "'-L/swift3Action/spm-build/.build/release' " \
+ "-o '/swift3Action/spm-build/.build/release/Action'"
+GENERATED_BUILD_SCRIPT = "/swift3Action/spm-build/swiftbuildandlink.sh"
+SPM_DIRECTORY = "/swift3Action/spm-build"
+BUILD_COMMAND = ["swift", "build", "-v", "-c", "release"]
+
+# Build Swift package and capture step trace
+print("Building action")
+out = check_output(BUILD_COMMAND, cwd=SPM_DIRECTORY)
+print("action built. Decoding compile and link commands")
+
+# Look for compile and link commands in step trace
+compileCommand = None
+linkCommand = None
+
+buildInstructions = out.decode("utf-8").splitlines()
+
+for instruction in buildInstructions:
+ if instruction.startswith(COMPILE_PREFIX):
+ compileCommand = instruction
+
+ # add flag to quiet warnings
+ compileCommand += " -suppress-warnings"
+
+ elif instruction.startswith(LINKER_PREFIX):
+ linkCommand = instruction
+
+# Create build script if found, exit otherwise
+if compileCommand is not None and linkCommand is not None:
+ print("Success, command and link commands found.")
+ with open(GENERATED_BUILD_SCRIPT, "a") as buildScript:
+ buildScript.write("#!/bin/bash\n")
+ buildScript.write("echo \"Compiling\"\n")
+ buildScript.write("%s\n" % compileCommand)
+ buildScript.write("swiftStatus=$?\n")
+ buildScript.write("echo swiftc status is $swiftStatus\n")
+ buildScript.write("if [[ \"$swiftStatus\" -eq \"0\" ]]; then\n")
+ buildScript.write("echo \"Linking\"\n")
+ buildScript.write("%s\n" % linkCommand)
+ buildScript.write("else\n")
+ buildScript.write(">&2 echo \"Action did not compile\"\n")
+ buildScript.write("exit 1\n")
+ buildScript.write("fi")
+
+ os.chmod(GENERATED_BUILD_SCRIPT, 0o777)
+ sys.exit(0)
+else:
+ print("Cannot generate build script: compile or link command not found")
+ sys.exit(1)
diff --git a/swift-playground-action/swift3Action/delete-build-run.sh b/swift-playground-action/swift3Action/delete-build-run.sh
new file mode 100755
index 0000000..430c02d
--- /dev/null
+++ b/swift-playground-action/swift3Action/delete-build-run.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+# Useful for local testing.
+# USE WITH CAUTION !!
+
+# This script is useful for testing the action proxy (or its derivatives)
+# in combination with [init,run].py. Use it to rebuild the container image
+# and start the proxy: delete-build-run.sh whisk/dockerskeleton.
+
+# Removes all previously built instances.
+remove=$(docker ps -a -q)
+if [[ ! -z $remove ]]; then
+ docker rm $remove
+fi
+
+image=${1:-openwhisk/dockerskeleton}
+docker build -t $image .
+
+echo ""
+echo " ---- RUNNING ---- "
+echo ""
+
+docker run -i -t -p 8080:8080 $image
diff --git a/swift-playground-action/swift3Action/epilogue.swift b/swift-playground-action/swift3Action/epilogue.swift
new file mode 100644
index 0000000..9107234
--- /dev/null
+++ b/swift-playground-action/swift3Action/epilogue.swift
@@ -0,0 +1,78 @@
+
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Deliberate whitespaces above.
+
+/* This code is appended to user-supplied action code.
+ It reads from the standard input, deserializes into JSON and invokes the
+ main function. Currently, actions print strings to stdout. This can evolve once
+ JSON serialization is available in Foundation. */
+
+import Foundation
+
+#if os(Linux)
+ import Glibc
+#endif
+
+func _whisk_json2dict(txt: String) -> [String:Any]? {
+ if let data = txt.data(using: String.Encoding.utf8, allowLossyConversion: true) {
+ do {
+ return WhiskJsonUtils.jsonDataToDictionary(jsonData: data)
+ } catch {
+ return nil
+ }
+ }
+ return nil
+}
+
+
+func _run_main(mainFunction: ([String: Any]) -> [String: Any]) -> Void {
+ let env = ProcessInfo.processInfo.environment
+ let inputStr: String = env["WHISK_INPUT"] ?? "{}"
+
+ if let parsed = _whisk_json2dict(txt: inputStr) {
+ let result = mainFunction(parsed)
+
+ if result is [String:Any] {
+ do {
+ if let respString = WhiskJsonUtils.dictionaryToJsonString(jsonDict: result) {
+ print("\(respString)")
+ } else {
+ print("Error converting \(result) to JSON string")
+ #if os(Linux)
+ fputs("Error converting \(result) to JSON string", stderr)
+ #endif
+ }
+ } catch {
+ print("Error serializing response \(error)")
+ #if os(Linux)
+ fputs("Error serializing response \(error)", stderr)
+ #endif
+ }
+ } else {
+ print("Cannot serialize response: \(result)")
+ #if os(Linux)
+ fputs("Cannot serialize response: \(result)", stderr)
+ #endif
+ }
+ } else {
+ print("Error: couldn't parse JSON input.")
+ #if os(Linux)
+ fputs("Error: couldn't parse JSON input.", stderr)
+ #endif
+ }
+}
diff --git a/swift-playground-action/swift3Action/invoke.py b/swift-playground-action/swift3Action/invoke.py
new file mode 100755
index 0000000..92d8a6d
--- /dev/null
+++ b/swift-playground-action/swift3Action/invoke.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+"""Executable Python script for testing the action proxy.
+
+ This script is useful for testing the action proxy (or its derivatives)
+ by simulating invoker interactions. Use it in combination with
+ delete-build-run.sh which builds and starts up the action proxy.
+ Examples:
+ ./delete-build-run.sh &
+ ./invoke.py init <action source file> # should return OK
+ ./invoke.py run '{"some":"json object as a string"}'
+/*
+ * 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.
+ */
+"""
+
+import os
+import re
+import sys
+import json
+import requests
+import codecs
+
+DOCKER_HOST = "localhost"
+if "DOCKER_HOST" in os.environ:
+ try:
+ DOCKER_HOST = re.compile("tcp://(.*):[\d]+").findall(
+ os.environ["DOCKER_HOST"])[0]
+ except Exception:
+ print("cannot determine docker host from %s" %
+ os.environ["DOCKER_HOST"])
+ sys.exit(-1)
+DEST = "http://%s:8080" % DOCKER_HOST
+
+
+def content_from_args(args):
+ if len(args) == 0:
+ return None
+
+ if len(args) == 1 and os.path.exists(args[0]):
+ with open(args[0]) as fp:
+ return json.load(fp)
+
+ # else...
+ in_str = " ".join(args)
+ try:
+ d = json.loads(in_str)
+ if isinstance(d, dict):
+ return d
+ else:
+ raise "Not a dict."
+ except:
+ return in_str
+
+
+def init(args):
+ main = args[1] if len(args) == 2 else "main"
+ args = args[0] if len(args) >= 1 else None
+
+ if args and args.endswith(".zip"):
+ with open(args, "rb") as fp:
+ contents = fp.read().encode("base64")
+ binary = True
+ elif args:
+ with(codecs.open(args, "r", "utf-8")) as fp:
+ contents = fp.read()
+ binary = False
+ else:
+ contents = None
+ binary = False
+
+ r = requests.post("%s/init" % DEST, json={"value": {"code": contents,
+ "binary": binary,
+ "main": main}})
+ print(r.text)
+
+
+def run(args):
+ value = content_from_args(args)
+ # print("Sending value: %s..." % json.dumps(value)[0:40])
+ r = requests.post("%s/run" % DEST, json={"value": value})
+ print(r.text)
+
+
+if sys.argv[1] == "init":
+ init(sys.argv[2:])
+elif sys.argv[1] == "run":
+ run(sys.argv[2:])
+else:
+ print("usage: 'init <filename>' or 'run JSON-as-string'")
diff --git a/swift-playground-action/swift3Action/spm-build/Package.swift b/swift-playground-action/swift3Action/spm-build/Package.swift
new file mode 100644
index 0000000..c4b964d
--- /dev/null
+++ b/swift-playground-action/swift3Action/spm-build/Package.swift
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import PackageDescription
+
+let package = Package(
+ name: "Action",
+ dependencies: [
+ .Package(url: "https://github.com/IBM-Swift/Kitura-net.git", "1.0.1"),
+ .Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", "14.2.0"),
+ .Package(url: "https://github.com/IBM-Swift/swift-watson-sdk.git", "0.4.1")
+ ]
+)
diff --git a/swift-playground-action/swift3Action/spm-build/_Whisk.swift b/swift-playground-action/swift3Action/spm-build/_Whisk.swift
new file mode 100644
index 0000000..d1124ed
--- /dev/null
+++ b/swift-playground-action/swift3Action/spm-build/_Whisk.swift
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import KituraNet
+import Dispatch
+
+class Whisk {
+ class func invoke(actionNamed action : String, withParameters params : [String:Any], blocking: Bool = true) -> [String:Any] {
+ let parsedAction = parseQualifiedName(name: action)
+ let strBlocking = blocking ? "true" : "false"
+ let path = "/api/v1/namespaces/\(parsedAction.namespace)/actions/\(parsedAction.name)?blocking=\(strBlocking)"
+
+ return postSyncronish(uriPath: path, params: params)
+ }
+
+ class func trigger(eventNamed event : String, withParameters params : [String:Any]) -> [String:Any] {
+ let parsedEvent = parseQualifiedName(name: event)
+ let path = "/api/v1/namespaces/\(parsedEvent.namespace)/triggers/\(parsedEvent.name)?blocking=true"
+
+ return postSyncronish(uriPath: path, params: params)
+ }
+
+ // handle the GCD dance to make the post async, but then obtain/return
+ // the result from this function sync
+ private class func postSyncronish(uriPath path: String, params : [String:Any]) -> [String:Any] {
+ var response : [String:Any]!
+
+ let queue = DispatchQueue.global()
+ let invokeGroup = DispatchGroup()
+
+ invokeGroup.enter()
+ queue.async {
+ post(uriPath: path, params: params, group: invokeGroup) { result in
+ response = result
+ }
+ }
+
+ // On one hand, FOREVER seems like an awfully long time...
+ // But on the other hand, I think we can rely on the system to kill this
+ // if it exceeds a reasonable execution time.
+ switch invokeGroup.wait(timeout: DispatchTime.distantFuture) {
+ case DispatchTimeoutResult.success:
+ break
+ case DispatchTimeoutResult.timedOut:
+ break
+ }
+
+ return response
+ }
+
+ /**
+ * Initializes with host, port and authKey determined from environment variables
+ * __OW_API_HOST and __OW_API_KEY, respectively.
+ */
+ private class func initializeCommunication() -> (httpType: String, host : String, port : Int16, authKey : String) {
+ let env = ProcessInfo.processInfo.environment
+
+ var edgeHost : String!
+ if let edgeHostEnv : String = env["__OW_API_HOST"] {
+ edgeHost = "\(edgeHostEnv)"
+ } else {
+ fatalError("__OW_API_HOST environment variable was not set.")
+ }
+
+ var protocolIndex = edgeHost.startIndex
+
+ //default to https
+ var httpType = "https://"
+ var port : Int16 = 443
+
+ // check if protocol is included in environment variable
+ if edgeHost.hasPrefix("https://") {
+ protocolIndex = edgeHost.index(edgeHost.startIndex, offsetBy: 8)
+ } else if edgeHost.hasPrefix("http://") {
+ protocolIndex = edgeHost.index(edgeHost.startIndex, offsetBy: 7)
+ httpType = "http://"
+ port = 80
+ }
+
+ let hostname = edgeHost.substring(from: protocolIndex)
+ let hostComponents = hostname.components(separatedBy: ":")
+
+ let host = hostComponents[0]
+
+ if hostComponents.count == 2 {
+ port = Int16(hostComponents[1])!
+ }
+
+ var authKey = "authKey"
+ if let authKeyEnv : String = env["__OW_API_KEY"] {
+ authKey = authKeyEnv
+ }
+
+ return (httpType, host, port, authKey)
+ }
+
+ // actually do the POST call to the specified OpenWhisk URI path
+ private class func post(uriPath: String, params : [String:Any], group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
+ let communicationDetails = initializeCommunication()
+
+ let loginData: Data = communicationDetails.authKey.data(using: String.Encoding.utf8, allowLossyConversion: false)!
+ let base64EncodedAuthKey = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
+
+ let headers = ["Content-Type" : "application/json",
+ "Authorization" : "Basic \(base64EncodedAuthKey)"]
+
+ guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
+ callback(["error": "Error encoding uri path to make openwhisk REST call."])
+ return
+ }
+
+ // TODO vary the schema based on the port?
+ let requestOptions = [ClientRequest.Options.schema(communicationDetails.httpType),
+ ClientRequest.Options.method("post"),
+ ClientRequest.Options.hostname(communicationDetails.host),
+ ClientRequest.Options.port(communicationDetails.port),
+ ClientRequest.Options.path(encodedPath),
+ ClientRequest.Options.headers(headers),
+ ClientRequest.Options.disableSSLVerification]
+
+ let request = HTTP.request(requestOptions) { response in
+
+ // exit group after we are done
+ defer {
+ group.leave()
+ }
+
+ if response != nil {
+ do {
+ // this is odd, but that's just how KituraNet has you get
+ // the response as NSData
+ var jsonData = Data()
+ try response!.readAllData(into: &jsonData)
+
+ switch WhiskJsonUtils.getJsonType(jsonData: jsonData) {
+ case .Dictionary:
+ if let resp = WhiskJsonUtils.jsonDataToDictionary(jsonData: jsonData) {
+ callback(resp)
+ } else {
+ callback(["error": "Could not parse a valid JSON response."])
+ }
+ case .Array:
+ if WhiskJsonUtils.jsonDataToArray(jsonData: jsonData) != nil {
+ callback(["error": "Response is an array, expecting dictionary."])
+ } else {
+ callback(["error": "Could not parse a valid JSON response."])
+ }
+ case .Undefined:
+ callback(["error": "Could not parse a valid JSON response."])
+ }
+
+ } catch {
+ callback(["error": "Could not parse a valid JSON response."])
+ }
+ } else {
+ callback(["error": "Did not receive a response."])
+ }
+ }
+
+ // turn params into JSON data
+ if let jsonData = WhiskJsonUtils.dictionaryToJsonString(jsonDict: params) {
+ request.write(from: jsonData)
+ request.end()
+ } else {
+ callback(["error": "Could not parse parameters."])
+ group.leave()
+ }
+
+ }
+
+ /**
+ * This function is currently unused but ready when we want to switch to using URLSession instead of KituraNet.
+ */
+ private class func postUrlSession(uriPath: String, params : [String:Any], group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
+ let communicationDetails = initializeCommunication()
+
+ guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
+ callback(["error": "Error encoding uri path to make openwhisk REST call."])
+ return
+ }
+
+ let urlStr = "\(communicationDetails.httpType)\(communicationDetails.host):\(communicationDetails.port)\(encodedPath)"
+
+ if let url = URL(string: urlStr) {
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+
+ do {
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.httpBody = try JSONSerialization.data(withJSONObject: params)
+
+ let loginData: Data = communicationDetails.authKey.data(using: String.Encoding.utf8, allowLossyConversion: false)!
+ let base64EncodedAuthKey = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
+ request.addValue("Basic \(base64EncodedAuthKey)", forHTTPHeaderField: "Authorization")
+
+ let session = URLSession(configuration: URLSessionConfiguration.default)
+
+ let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
+
+ // exit group after we are done
+ defer {
+ group.leave()
+ }
+
+ if let error = error {
+ callback(["error":error.localizedDescription])
+ } else {
+
+ if let data = data {
+ do {
+ let respJson = try JSONSerialization.jsonObject(with: data)
+ if respJson is [String:Any] {
+ callback(respJson as! [String:Any])
+ } else {
+ callback(["error":" response from server is not a dictionary"])
+ }
+ } catch {
+ callback(["error":"Error creating json from response: \(error)"])
+ }
+ }
+ }
+ })
+
+ task.resume()
+ } catch {
+ callback(["error":"Got error creating params body: \(error)"])
+ }
+ }
+ }
+
+ // separate an OpenWhisk qualified name (e.g. "/whisk.system/samples/date")
+ // into namespace and name components
+ private class func parseQualifiedName(name qualifiedName : String) -> (namespace : String, name : String) {
+ let defaultNamespace = "_"
+ let delimiter = "/"
+
+ let segments :[String] = qualifiedName.components(separatedBy: delimiter)
+
+ if segments.count > 2 {
+ return (segments[1], Array(segments[2..<segments.count]).joined(separator: delimiter))
+ } else {
+ // allow both "/theName" and "theName"
+ let name = qualifiedName.hasPrefix(delimiter) ? segments[1] : segments[0]
+ return (defaultNamespace, name)
+ }
+ }
+}
diff --git a/swift-playground-action/swift3Action/spm-build/_WhiskJSONUtils.swift b/swift-playground-action/swift3Action/spm-build/_WhiskJSONUtils.swift
new file mode 100644
index 0000000..f9e6460
--- /dev/null
+++ b/swift-playground-action/swift3Action/spm-build/_WhiskJSONUtils.swift
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import SwiftyJSON
+
+enum WhiskJsonType {
+ case Dictionary
+ case Array
+ case Undefined
+}
+
+class WhiskJsonUtils {
+
+ class func getJsonType(jsonData: Data) -> WhiskJsonType {
+ do {
+ let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
+ if json is [String:Any] {
+ return .Dictionary
+ } else if json is [Any] {
+ return .Array
+ } else {
+ return .Undefined
+ }
+ } catch {
+ print("Error converting JSON data to dictionary \(error)")
+ return .Undefined
+ }
+ }
+
+ class func jsonDataToArray(jsonData: Data) -> [Any]? {
+ do {
+ let arr = try JSONSerialization.jsonObject(with: jsonData, options: [])
+ return (arr as? [Any])
+ } catch {
+ print("Error converting JSON data to dictionary \(error)")
+ return nil
+ }
+ }
+
+ class func jsonDataToDictionary(jsonData: Data) -> [String:Any]? {
+ do {
+ let dic = try JSONSerialization.jsonObject(with: jsonData, options: [])
+ return dic as? [String:Any]
+ } catch {
+ print("Error converting JSON data to dictionary \(error)")
+ return nil
+ }
+ }
+
+ // use SwiftyJSON to serialize JSON object because of bug in Linux Swift 3.0
+ // https://github.com/IBM-Swift/SwiftRuntime/issues/230
+ class func dictionaryToJsonString(jsonDict: [String:Any]) -> String? {
+ if JSONSerialization.isValidJSONObject(jsonDict) {
+ do {
+ let jsonData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
+ if let jsonStr = String(data: jsonData, encoding: String.Encoding.utf8) {
+ return jsonStr
+ } else {
+ print("Error serializing data to JSON, data conversion returns nil string")
+ }
+ } catch {
+ print(("\(error)"))
+ }
+ } else {
+ print("Error serializing JSON, data does not appear to be valid JSON")
+ }
+ return nil
+ }
+
+ class func dictionaryToData(jsonDict: [String:Any]) -> Data? {
+ let json: JSON = JSON(jsonDict)
+
+ do {
+ let data: Data = try json.rawData()
+ return data
+ } catch {
+ print("Cannot convert Dictionary to Data")
+ return nil
+ }
+ }
+
+ class func arrayToJsonString(jsonArray: [Any]) -> String? {
+ let json: JSON = JSON(jsonArray)
+
+ if let jsonStr = json.rawString() {
+ let trimmed = jsonStr.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\r", with: "")
+ return trimmed
+ } else {
+ return nil
+ }
+ }
+
+ class func arrayToData(jsonArray: [Any]) -> Data? {
+ let json: JSON = JSON(jsonArray)
+
+ do {
+ let data: Data = try json.rawData()
+ return data
+ } catch {
+ print("Cannot convert Array to Data")
+ return nil
+ }
+ }
+
+ private class func escapeDict(json: [String:Any]) -> [String:Any] {
+ var escaped = [String:Any]()
+
+ for (k,v) in json {
+ if v is String {
+ let str = (v as! String).replacingOccurrences(of:"\"", with:"\\\"")
+ escaped[k] = str
+ } else if v is [String:Any] {
+ escaped[k] = escapeDict(json: v as! [String : Any])
+ } else if v is [Any] {
+ escaped[k] = escapeArray(json: v as! [Any])
+ } else {
+ escaped[k] = v
+ }
+ }
+ return escaped
+ }
+
+ private class func escapeArray(json: [Any]) -> [Any] {
+ var escaped = [Any]()
+
+ for v in json {
+ if v is String {
+ let str = (v as! String).replacingOccurrences(of:"\"", with:"\\\"")
+ escaped.append(str)
+ } else if v is [String:Any] {
+ let dic = escapeDict(json: v as! [String:Any])
+ escaped.append(dic)
+ } else if v is [Any] {
+ let arr = escapeArray(json: v as! [Any])
+ escaped.append(arr)
+ } else {
+ escaped.append(v)
+ }
+ }
+
+ return escaped
+ }
+
+ private class func escape(json: Any) -> Any? {
+ if json is [String:Any] {
+ let escapeObj = json as! [String:Any]
+ return escapeDict(json: escapeObj)
+ } else if json is [Any] {
+ let escapeObj = json as! [Any]
+ return escapeArray(json: escapeObj)
+ } else {
+ return nil
+ }
+ }
+}
diff --git a/swift-playground-action/swift3Action/stub.sh b/swift-playground-action/swift3Action/stub.sh
new file mode 100644
index 0000000..842d00a
--- /dev/null
+++ b/swift-playground-action/swift3Action/stub.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+echo \
+'This is a stub action that should be replaced with user code (e.g., script or compatible binary).
+The input to the action is received as an argument from the command line.
+Actions may log to stdout or stderr. By convention, the last line of output must
+be a stringified JSON object which represents the result of the action.'
+
+echo '{ "error": "This is a stub action. Replace it with custom logic." }'
\ No newline at end of file
diff --git a/swift-playground-action/swift3Action/swift3runner.py b/swift-playground-action/swift3Action/swift3runner.py
new file mode 100644
index 0000000..f41456d
--- /dev/null
+++ b/swift-playground-action/swift3Action/swift3runner.py
@@ -0,0 +1,114 @@
+"""Python proxy to run Swift action.
+
+/*
+ * 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.
+ */
+"""
+import os
+import glob
+import sys
+import subprocess
+import codecs
+import json
+sys.path.append('../actionProxy')
+from actionproxy import ActionRunner, main, setRunner # noqa
+
+SRC_EPILOGUE_FILE = '/swift3Action/epilogue.swift'
+DEST_SCRIPT_FILE = '/swift3Action/spm-build/main.swift'
+DEST_SCRIPT_DIR = '/swift3Action/spm-build'
+DEST_BIN_FILE = '/swift3Action/spm-build/.build/release/Action'
+
+BUILD_PROCESS = ['./swiftbuildandlink.sh']
+
+
+class Swift3Runner(ActionRunner):
+
+ def __init__(self):
+ ActionRunner.__init__(self, DEST_SCRIPT_FILE, DEST_BIN_FILE)
+
+ # remove pre-existing binary before receiving a new binary
+ def preinit(self):
+ try:
+ os.remove(self.binary)
+ except: pass
+
+ def epilogue(self, init_message):
+ # skip if executable already exists (was unzipped)
+ if os.path.isfile(self.binary):
+ return
+
+ if 'main' in init_message:
+ main_function = init_message['main']
+ else:
+ main_function = 'main'
+ # make sure there is a main.swift file
+ open(DEST_SCRIPT_FILE, 'a').close()
+
+ with codecs.open(DEST_SCRIPT_FILE, 'a', 'utf-8') as fp:
+ os.chdir(DEST_SCRIPT_DIR)
+ for file in glob.glob("*.swift"):
+ if file not in ["Package.swift", "main.swift", "_WhiskJSONUtils.swift", "_Whisk.swift"]:
+ with codecs.open(file, 'r', 'utf-8') as f:
+ fp.write(f.read())
+ with codecs.open(SRC_EPILOGUE_FILE, 'r', 'utf-8') as ep:
+ fp.write(ep.read())
+
+ fp.write('_run_main(mainFunction: %s)\n' % main_function)
+
+ def build(self, init_message):
+ # short circuit the build, if there already exists a binary
+ # from the zip file
+ if os.path.isfile(self.binary):
+ # file may not have executable permission, set it
+ os.chmod(self.binary, 0o555)
+ return
+
+ p = subprocess.Popen(
+ BUILD_PROCESS,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd=DEST_SCRIPT_DIR)
+
+ # run the process and wait until it completes.
+ # stdout/stderr will not be None because we passed PIPEs to Popen
+ (o, e) = p.communicate()
+
+ # stdout/stderr may be either text or bytes, depending on Python
+ # version, so if bytes, decode to text. Note that in Python 2
+ # a string will match both types; so also skip decoding in that case
+ if isinstance(o, bytes) and not isinstance(o, str):
+ o = o.decode('utf-8')
+ if isinstance(e, bytes) and not isinstance(e, str):
+ e = e.decode('utf-8')
+
+ if o:
+ sys.stdout.write(o)
+ sys.stdout.flush()
+
+ if e:
+ sys.stderr.write(e)
+ sys.stderr.flush()
+
+ def env(self, message):
+ env = ActionRunner.env(self, message)
+ args = message.get('value', {}) if message else {}
+ env['WHISK_INPUT'] = json.dumps(args)
+ return env
+
+
+if __name__ == '__main__':
+ setRunner(Swift3Runner())
+ main()
diff --git a/swift-playground-action/test0.swift b/swift-playground-action/test0.swift
new file mode 100644
index 0000000..1c6ee55
--- /dev/null
+++ b/swift-playground-action/test0.swift
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+print("Hello World");
diff --git a/swift-playground-action/test1.swift b/swift-playground-action/test1.swift
new file mode 100644
index 0000000..8dc17c7
--- /dev/null
+++ b/swift-playground-action/test1.swift
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+
+//let json: JSON = "I'm a json"
+let json = JSON(["name":"Joe Doe", "age": 25])
+if let name = json["name"].string {
+ print("Hello", name);
+}
diff --git a/swift-playground-action/test2.swift b/swift-playground-action/test2.swift
new file mode 100644
index 0000000..15df760
--- /dev/null
+++ b/swift-playground-action/test2.swift
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+import KituraNet
+import Dispatch
+import SSLService
+
+//let json: JSON = "I'm a json"
+// let json = JSON(["name":"Paul", "age": 25])
+// if let name = json["name"].string {
+// print("Hello ", name);
+// }
diff --git a/swift-playground-action/test3.swift b/swift-playground-action/test3.swift
new file mode 100644
index 0000000..97c0e1e
--- /dev/null
+++ b/swift-playground-action/test3.swift
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+import Conversation
+import RestKit
+
+//let json: JSON = "I'm a json"
+// let json = JSON(["name":"Paul", "age": 25])
+// if let name = json["name"].string {
+// print("Hello ", name);
+// }
+
+let username = "aa50bee6-a71b-47d3-8dca-e23b628315e3"
+let password = "mtqVUlA5WCZy"
+let version = "2016-07-19" // use today's date for the most recent version
+let conversation = Conversation(username: username, password: password, version: version)
+
+let workspaceID = "b4ff2246-c42f-407e-b172-72a631cf5498"
+let failure = { (error: RestError) in print("error", error) }
+var context: Context? // save context to continue conversation
+var text = "hello"
+conversation.message(workspaceID: workspaceID, text: text, failure: failure) { response in
+ //print(response.output.text)
+ print("response",response)
+ context = response.context
+}
+
+// conversation.message(workspaceID: workspaceID, text: text, context: context, failure: failure) { response in
+// print(response.output.text)
+// context = response.context
+// }
diff --git a/swift-playground-action/test4.swift b/swift-playground-action/test4.swift
new file mode 100644
index 0000000..ea3ff8a
--- /dev/null
+++ b/swift-playground-action/test4.swift
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//import Dispatch
+import Foundation
+import SwiftyJSON
+import Conversation
+import RestKit
+
+//var serviceURL = "https://jsonplaceholder.typicode.com"
+
+let username = "aa50bee6-a71b-47d3-8dca-e23b628315e3"
+let password = "mtqVUlA5WCZy"
+let version = "2016-07-19"
+
+var serviceURL = "https://gateway.watsonplatform.net/conversation/api"
+
+var queryParameters = [URLQueryItem]()
+queryParameters.append(URLQueryItem(name: "version", value: version))
+
+let request = RestRequest(
+ method: .GET,
+ url: serviceURL + "/v1/workspaces",
+ acceptType: "application/json",
+ contentType: "application/json",
+ queryParameters: queryParameters,
+ username: username,
+ password: password
+ )
+
+let failure = { (error: RestError) in print("error", error) }
+
+let success = { (msg) in print("success", msg) }
+
+// execute REST request
+request.responseJSON { response in
+ switch response {
+ case .success(let json): success(json)
+ case .failure(let error): failure(error)
+ }
+}