diff --git a/tljh-voila-gallery/setup.py b/tljh-voila-gallery/setup.py index a29b426..59e5c3a 100644 --- a/tljh-voila-gallery/setup.py +++ b/tljh-voila-gallery/setup.py @@ -7,5 +7,12 @@ "console_scripts": ["build-gallery-images = tljh_voila_gallery.build_images:main"] }, packages=find_packages(), - include_package_data=True + include_package_data=True, + install_requires=[ + # These get installed into the hub environment + 'dockerspawner', + 'jupyter-repo2docker', + 'binderhub', + 'nullauthenticator' + ] ) diff --git a/tljh-voila-gallery/tljh_voila_gallery/__init__.py b/tljh-voila-gallery/tljh_voila_gallery/__init__.py index 8828ecc..2795836 100644 --- a/tljh-voila-gallery/tljh_voila_gallery/__init__.py +++ b/tljh-voila-gallery/tljh_voila_gallery/__init__.py @@ -1,4 +1,5 @@ import socket +import sys import os import jinja2 from pkg_resources import resource_stream, resource_filename @@ -29,26 +30,41 @@ def options_form(spawner): @hookimpl def tljh_custom_jupyterhub_config(c): - # Since dockerspawner isn't available at import time from dockerspawner import DockerSpawner + from nullauthenticator import NullAuthenticator class GallerySpawner(DockerSpawner): - def options_from_form(self, formdata): - options = {} - if 'example' in formdata: - options['example'] = formdata['example'][0] - return options + # FIXME: What to do about idle culling?! + cmd = 'jupyter-notebook' + + events = False + + def get_args(self): + args = [ + '--ip=0.0.0.0', + '--port=%i' % self.port, + '--NotebookApp.base_url=%s' % self.server.base_url, + '--NotebookApp.token=%s' % self.user_options['token'], + '--NotebookApp.trust_xheaders=True', + ] + return args + self.args def start(self): - gallery = get_gallery() - examples = gallery['examples'] - chosen_example = self.user_options['example'] - assert chosen_example in examples - self.default_url = examples[chosen_example]['url'] - self.image = examples[chosen_example]['image'] + if 'token' not in self.user_options: + raise web.HTTPError(400, "token required") + if 'image' not in self.user_options: + raise web.HTTPError(400, "image required") + self.image = self.user_options['image'] return super().start() + + class GalleryAuthenticator(NullAuthenticator): + auto_login = True + + def login_url(self, base_url): + return '/services/gallery' + c.JupyterHub.spawner_class = GallerySpawner - c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator' + c.JupyterHub.authenticator_class = GalleryAuthenticator c.JupyterHub.hub_connect_ip = socket.gethostname() @@ -57,23 +73,15 @@ def start(self): # rm containers when they stop c.DockerSpawner.remove = True - # Disabled until we fix it in TmpAuthenticator - # c.TmpAuthenticator.force_new_server = True - - c.DockerSpawner.cmd = ['jupyterhub-singleuser'] - # Override JupyterHub template - c.JupyterHub.template_paths = [TEMPLATES_PATH] - - c.Spawner.options_form = options_form - - -@hookimpl -def tljh_extra_hub_pip_packages(): - return [ - 'dockerspawner', - 'git+https://github.com/jupyter/repo2docker.git@f19e159dfe1006dbd82c7728e15cdd19751e8aec' - ] + c.JupyterHub.services = [{ + 'name': 'gallery', + 'admin': True, + 'url': 'http://127.0.0.1:9888', + 'command': [ + sys.executable, '-m', 'tljh_voila_gallery.gallery' + ] + }] @hookimpl def tljh_extra_apt_packages(): diff --git a/tljh-voila-gallery/tljh_voila_gallery/gallery.py b/tljh-voila-gallery/tljh_voila_gallery/gallery.py new file mode 100644 index 0000000..6ef529f --- /dev/null +++ b/tljh-voila-gallery/tljh_voila_gallery/gallery.py @@ -0,0 +1,62 @@ +import os +from tornado import ioloop, web +from pkg_resources import resource_stream, resource_filename +from ruamel.yaml import YAML +from jinja2 import PackageLoader, Environment +import json +from binderhub.launcher import Launcher +from urllib.parse import urljoin, urlencode + +yaml = YAML() + +# Read gallery.yaml on each spawn. If this gets too expensive, cache it here +def get_gallery(): + with resource_stream(__name__, 'gallery.yaml') as f: + return yaml.load(f) + +templates = Environment(loader=PackageLoader('tljh_voila_gallery', 'templates')) + +class GalleryHandler(web.RequestHandler): + def get(self): + gallery_template = templates.get_template('gallery-examples.html') + gallery = get_gallery() + + self.write(gallery_template.render( + url=self.request.full_url(), + examples=gallery.get('examples', []) + )) + + async def post(self): + gallery = get_gallery() + + example_name = self.get_body_argument('example') + + example = gallery['examples'][example_name] + + + launcher = Launcher( + hub_api_token=os.environ['JUPYTERHUB_API_TOKEN'], + hub_url=os.environ['JUPYTERHUB_BASE_URL'] + ) + response = await launcher.launch( + example['image'], + launcher.unique_name_from_repo(example['repo_url']) + ) + redirect_url = urljoin( + response['url'], + example['url'] + ) + '?' + urlencode({'token': response['token']}) + self.redirect(redirect_url) + + +def make_app(): + return web.Application([ + (os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', GalleryHandler), + ]) + +if __name__ == "__main__": + if not os.environ['JUPYTERHUB_API_URL'].endswith('/'): + os.environ['JUPYTERHUB_API_URL'] = os.environ['JUPYTERHUB_API_URL'] + '/' + app = make_app() + app.listen(9888) + ioloop.IOLoop.current().start() \ No newline at end of file diff --git a/tljh-voila-gallery/tljh_voila_gallery/gallery.yaml b/tljh-voila-gallery/tljh_voila_gallery/gallery.yaml index 0b14ec2..327258b 100644 --- a/tljh-voila-gallery/tljh_voila_gallery/gallery.yaml +++ b/tljh-voila-gallery/tljh_voila_gallery/gallery.yaml @@ -3,20 +3,20 @@ examples: title: country-indicators description: Explore the correlations between indicators of development using matplotlib and ipywidgets image: country-indicators:latest - url: /voila/render/index.ipynb + url: voila/render/index.ipynb repo_url: https://github.com/voila-gallery/voila-gallery-country-indicators image_url: https://i.imgur.com/IeG75O3.png gaussian-density: title: gaussian-density description: Explore the normal distribution interactively with bqplot image: gaussian-density:latest - url: /voila/render/index.ipynb + url: voila/render/index.ipynb repo_url: https://github.com/voila-gallery/gaussian-density image_url: https://i.imgur.com/J1Mj6rc.png render-stl: title: render-stl description: Explore STL files with ipyvolume image: render-stl:latest - url: /voila/render/index.ipynb + url: voila/render/index.ipynb repo_url: https://github.com/voila-gallery/render-stl image_url: https://github.com/voila-gallery/render-stl/raw/master/thumbnail.png diff --git a/tljh-voila-gallery/tljh_voila_gallery/templates/options_form.html b/tljh-voila-gallery/tljh_voila_gallery/templates/gallery-examples.html similarity index 83% rename from tljh-voila-gallery/tljh_voila_gallery/templates/options_form.html rename to tljh-voila-gallery/tljh_voila_gallery/templates/gallery-examples.html index 380a83f..296e18f 100644 --- a/tljh-voila-gallery/tljh_voila_gallery/templates/options_form.html +++ b/tljh-voila-gallery/tljh_voila_gallery/templates/gallery-examples.html @@ -1,3 +1,7 @@ +{% extends "page.html" %} + +{% block body %} +
{% for example_name, info in examples.items() %}
@@ -18,3 +22,7 @@

{{ info.title }}

{% endfor %}
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/tljh-voila-gallery/tljh_voila_gallery/templates/spawn.html b/tljh-voila-gallery/tljh_voila_gallery/templates/page.html similarity index 95% rename from tljh-voila-gallery/tljh_voila_gallery/templates/spawn.html rename to tljh-voila-gallery/tljh_voila_gallery/templates/page.html index 7616157..365b9d0 100644 --- a/tljh-voila-gallery/tljh_voila_gallery/templates/spawn.html +++ b/tljh-voila-gallery/tljh_voila_gallery/templates/page.html @@ -81,10 +81,8 @@

Voila Dashboards Gallery

- -
- {{spawner_options_form | safe}} -
+ {% block body %} + {% endblock %}