Add 'apps/arv-web/' from commit 'f9732ad8460d013c2f28363655d0d1b91894dca5'
authorPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 16 Jan 2015 19:05:48 +0000 (14:05 -0500)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 16 Jan 2015 19:05:48 +0000 (14:05 -0500)
git-subtree-dir: apps/arv-web
git-subtree-mainline: b97ac7f96234cbbb491bdbaade840ab50802f357
git-subtree-split: f9732ad8460d013c2f28363655d0d1b91894dca5

16 files changed:
apps/arv-web/Dockerfile [new file with mode: 0644]
apps/arv-web/README [new file with mode: 0644]
apps/arv-web/apache2_foreground.sh [new file with mode: 0755]
apps/arv-web/apache2_vhost [new file with mode: 0644]
apps/arv-web/arv-web.py [new file with mode: 0644]
apps/arv-web/sample-cgi-app/public/.htaccess [new file with mode: 0644]
apps/arv-web/sample-cgi-app/public/index.cgi [new file with mode: 0755]
apps/arv-web/sample-cgi-app/tmp/.keepkeep [new file with mode: 0644]
apps/arv-web/sample-rack-app/config.ru [new file with mode: 0644]
apps/arv-web/sample-rack-app/public/.keepkeep [new file with mode: 0644]
apps/arv-web/sample-rack-app/tmp/.keepkeep [new file with mode: 0644]
apps/arv-web/sample-static-page/public/index.html [new file with mode: 0644]
apps/arv-web/sample-static-page/tmp/.keepkeep [new file with mode: 0644]
apps/arv-web/sample-wsgi-app/passenger_wsgi.py [new file with mode: 0644]
apps/arv-web/sample-wsgi-app/public/.keepkeep [new file with mode: 0644]
apps/arv-web/sample-wsgi-app/tmp/.keepkeep [new file with mode: 0644]

diff --git a/apps/arv-web/Dockerfile b/apps/arv-web/Dockerfile
new file mode 100644 (file)
index 0000000..d0a6abe
--- /dev/null
@@ -0,0 +1,27 @@
+FROM arvados/base
+MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+
+RUN apt-get update -qq
+RUN apt-get install -qqy \
+        apt-utils git curl procps apache2-mpm-worker \
+        libcurl4-openssl-dev apache2-threaded-dev \
+        libapr1-dev libaprutil1-dev
+
+RUN cd /usr/src/arvados/services/api && \
+    /usr/local/rvm/bin/rvm-exec default bundle exec passenger-install-apache2-module --auto --languages ruby,python
+
+RUN cd /usr/src/arvados/services/api && \
+    /usr/local/rvm/bin/rvm-exec default bundle exec passenger-install-apache2-module --snippet > /etc/apache2/conf.d/passenger
+
+ADD apache2_foreground.sh /etc/apache2/foreground.sh
+
+ADD apache2_vhost /etc/apache2/sites-available/arv-web
+RUN \
+  mkdir /var/run/apache2 && \
+  a2dissite default && \
+  a2ensite arv-web && \
+  a2enmod rewrite
+
+EXPOSE 80
+
+CMD ["/etc/apache2/foreground.sh"]
\ No newline at end of file
diff --git a/apps/arv-web/README b/apps/arv-web/README
new file mode 100644 (file)
index 0000000..826092d
--- /dev/null
@@ -0,0 +1,28 @@
+Run a web service from Arvados.
+
+usage: arv-web.py [-h] --project PROJECT [--port PORT] --image IMAGE
+
+optional arguments:
+  -h, --help         show this help message and exit
+  --project PROJECT  Project to watch
+  --port PORT        Local bind port
+  --image IMAGE      Docker image to run
+
+
+This queries an Arvados project and FUSE mounts the most recently modified
+collection into a temporary directory.  It then runs the supplied Docker image
+with the collection bind mounted to /mnt inside the container.
+
+When a new collection is added to the project, or an existing project is
+updated, it will detect the change, it will stop the running Docker container,
+unmount the old collection, mount the new most recently modified collection,
+and restart the Docker container with the new mount.
+
+The supplied Dockerfile builds a Docker image that runs Apache with /mnt as the
+DocumentRoot.  It is configured to run web applications based on Python WSGI,
+Ruby Rack, CGI, to serve static HTML, or simply browse the contents of the
+/public subdirectory of the collection using Apache's default index pages.
+
+To build the Docker image:
+
+$ docker build -t arvados/arv-web .
diff --git a/apps/arv-web/apache2_foreground.sh b/apps/arv-web/apache2_foreground.sh
new file mode 100755 (executable)
index 0000000..fc6028e
--- /dev/null
@@ -0,0 +1,7 @@
+#! /bin/bash
+
+read pid cmd state ppid pgrp session tty_nr tpgid rest < /proc/self/stat
+trap "kill -TERM -$pgrp; exit" EXIT TERM KILL SIGKILL SIGTERM SIGQUIT
+
+source /etc/apache2/envvars
+/usr/sbin/apache2 -D FOREGROUND
diff --git a/apps/arv-web/apache2_vhost b/apps/arv-web/apache2_vhost
new file mode 100644 (file)
index 0000000..dad4a9b
--- /dev/null
@@ -0,0 +1,23 @@
+<VirtualHost *:80>
+
+  ServerName arv-web
+  ServerAdmin sysadmin@curoverse.com
+
+  # Index file and Document Root (where the public files are located)
+  DirectoryIndex index.html
+  DocumentRoot /mnt/public
+  RackBaseURI /
+
+  LogLevel warn
+  ErrorLog  ${APACHE_LOG_DIR}/error.log
+  CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+  <Directory /mnt/public>
+    Options Indexes IncludesNoExec
+    Options -MultiViews
+    AllowOverride All
+    Order allow,deny
+    Allow from all
+  </Directory>
+
+</VirtualHost>
diff --git a/apps/arv-web/arv-web.py b/apps/arv-web/arv-web.py
new file mode 100644 (file)
index 0000000..63d760d
--- /dev/null
@@ -0,0 +1,126 @@
+import arvados
+import subprocess
+from arvados_fuse import Operations, SafeApi, CollectionDirectory
+import tempfile
+import os
+import llfuse
+import threading
+import Queue
+import argparse
+import logging
+import signal
+import sys
+
+logging.basicConfig(level=logging.INFO)
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--project', type=str, required=True, help="Project to watch")
+parser.add_argument('--port', type=int, default=8080, help="Local bind port")
+parser.add_argument('--image', type=str, required=True, help="Docker image to run")
+
+args = parser.parse_args()
+
+api = SafeApi(arvados.config)
+project = args.project
+docker_image = args.image
+port = args.port
+evqueue = Queue.Queue()
+
+def run_fuse_mount(collection):
+    global api
+
+    mountdir = tempfile.mkdtemp()
+
+    operations = Operations(os.getuid(), os.getgid(), "utf-8", True)
+    operations.inodes.add_entry(CollectionDirectory(llfuse.ROOT_INODE, operations.inodes, api, 2, collection))
+
+    # Initialize the fuse connection
+    llfuse.init(operations, mountdir, ['allow_other'])
+
+    t = threading.Thread(None, lambda: llfuse.main())
+    t.start()
+
+    # wait until the driver is finished initializing
+    operations.initlock.wait()
+
+    return mountdir
+
+def on_message(ev):
+    global project
+    global evqueue
+
+    if 'event_type' in ev:
+        old_attr = None
+        if 'old_attributes' in ev['properties'] and ev['properties']['old_attributes']:
+            old_attr = ev['properties']['old_attributes']
+        if project not in (ev['properties']['new_attributes']['owner_uuid'],
+                                old_attr['owner_uuid'] if old_attr else None):
+            return
+
+        et = ev['event_type']
+        if ev['event_type'] == 'update' and ev['properties']['new_attributes']['owner_uuid'] != ev['properties']['old_attributes']['owner_uuid']:
+            if args.project == ev['properties']['new_attributes']['owner_uuid']:
+                et = 'add'
+            else:
+                et = 'remove'
+
+        evqueue.put((project, et, ev['object_uuid']))
+
+collection = api.collections().list(filters=[["owner_uuid", "=", project]],
+                    limit=1,
+                    order='modified_at desc').execute()['items'][0]['uuid']
+
+ws = arvados.events.subscribe(api, [["object_uuid", "is_a", "arvados#collection"]], on_message)
+
+signal.signal(signal.SIGTERM, lambda signal, frame: sys.exit(0))
+
+loop = True
+cid = None
+while loop:
+    logging.info("Mounting %s" % collection)
+    mountdir = run_fuse_mount(collection)
+    try:
+        logging.info("Starting docker container")
+        cid = subprocess.check_output(["docker", "run",
+                                       "--detach=true",
+                                       "--publish=%i:80" % (port),
+                                       "--volume=%s:/mnt:ro" % mountdir,
+                                       docker_image])
+        cid = cid.rstrip()
+        logging.info("Container id is %s" % cid)
+
+        logging.info("Waiting for events")
+        running = True
+        while running:
+            try:
+                eq = evqueue.get(True, 1)
+                logging.info("%s %s" % (eq[1], eq[2]))
+                newcollection = collection
+                if eq[1] in ('add', 'update', 'create'):
+                    newcollection = eq[2]
+                elif eq[1] == 'remove':
+                    newcollection = api.collections().list(filters=[["owner_uuid", "=", project]],
+                                                        limit=1,
+                                                        order='modified_at desc').execute()['items'][0]['uuid']
+                if newcollection != collection:
+                    logging.info("restarting web service")
+                    collection = newcollection
+                    running = False
+            except Queue.Empty:
+                pass
+    except (KeyboardInterrupt):
+        logging.info("Got keyboard interrupt")
+        ws.close()
+        loop = False
+    except Exception as e:
+        logging.exception(str(e))
+        ws.close()
+        loop = False
+    finally:
+        if cid:
+            logging.info("Stopping docker container")
+            cid = subprocess.call(["docker", "stop", cid])
+
+        logging.info("Unmounting")
+        subprocess.call(["fusermount", "-u", "-z", mountdir])
+        os.rmdir(mountdir)
diff --git a/apps/arv-web/sample-cgi-app/public/.htaccess b/apps/arv-web/sample-cgi-app/public/.htaccess
new file mode 100644 (file)
index 0000000..e5145bd
--- /dev/null
@@ -0,0 +1,3 @@
+Options +ExecCGI
+AddHandler cgi-script .cgi
+DirectoryIndex index.cgi
diff --git a/apps/arv-web/sample-cgi-app/public/index.cgi b/apps/arv-web/sample-cgi-app/public/index.cgi
new file mode 100755 (executable)
index 0000000..57bc2a9
--- /dev/null
@@ -0,0 +1,4 @@
+#!/usr/bin/perl
+
+print "Content-type: text/html\n\n";
+print "Hello world from perl!";
diff --git a/apps/arv-web/sample-cgi-app/tmp/.keepkeep b/apps/arv-web/sample-cgi-app/tmp/.keepkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/arv-web/sample-rack-app/config.ru b/apps/arv-web/sample-rack-app/config.ru
new file mode 100644 (file)
index 0000000..84bb0da
--- /dev/null
@@ -0,0 +1,4 @@
+app = proc do |env|
+    [200, { "Content-Type" => "text/html" }, ["hello <b>world</b> from ruby"]]
+end
+run app
diff --git a/apps/arv-web/sample-rack-app/public/.keepkeep b/apps/arv-web/sample-rack-app/public/.keepkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/arv-web/sample-rack-app/tmp/.keepkeep b/apps/arv-web/sample-rack-app/tmp/.keepkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/arv-web/sample-static-page/public/index.html b/apps/arv-web/sample-static-page/public/index.html
new file mode 100644 (file)
index 0000000..a2e485c
--- /dev/null
@@ -0,0 +1,6 @@
+<html>
+  <head><title>arv-web sample</title></head>
+  <body>
+    <p>Hello world static page</p>
+  </body>
+</html>
diff --git a/apps/arv-web/sample-static-page/tmp/.keepkeep b/apps/arv-web/sample-static-page/tmp/.keepkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/arv-web/sample-wsgi-app/passenger_wsgi.py b/apps/arv-web/sample-wsgi-app/passenger_wsgi.py
new file mode 100644 (file)
index 0000000..ea918f0
--- /dev/null
@@ -0,0 +1,3 @@
+def application(environ, start_response):
+    start_response('200 OK', [('Content-Type', 'text/plain')])
+    return [b"hello world from python!\n"]
diff --git a/apps/arv-web/sample-wsgi-app/public/.keepkeep b/apps/arv-web/sample-wsgi-app/public/.keepkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/arv-web/sample-wsgi-app/tmp/.keepkeep b/apps/arv-web/sample-wsgi-app/tmp/.keepkeep
new file mode 100644 (file)
index 0000000..e69de29