--- /dev/null
+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
--- /dev/null
+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 .
--- /dev/null
+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)