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