From 2e52cb7c60e487e72dc954dd5d7329fde43c7b3b Mon Sep 17 00:00:00 2001 From: Min RK Date: Sat, 10 Feb 2018 12:13:24 +0100 Subject: [PATCH] add image_whitelist and default options form if c.DockerSpawner.image_whitelist is specified, a default options form is constructed with a select dropdown for choosing the image to launch. image_whitelist is a dict of key: image, where the key is the value to be used in the form, while the values are the actual docker images. image_whitelist may be specified as a list, in which case it is cast to a dict of the form {image:image}, where the keys are the same as the values (users see actual image names, rather than abbreviations/descriptions). --- dockerspawner/dockerspawner.py | 82 +++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/dockerspawner/dockerspawner.py b/dockerspawner/dockerspawner.py index 4a0c5f8a..4286a707 100644 --- a/dockerspawner/dockerspawner.py +++ b/dockerspawner/dockerspawner.py @@ -11,18 +11,21 @@ import docker from docker.errors import APIError from docker.utils import kwargs_from_env -from tornado import gen +from tornado import gen, web from escapism import escape from jupyterhub.spawner import Spawner from traitlets import ( - Dict, - Unicode, + Any, Bool, + Dict, + List, Int, - Any, + Unicode, + Union, default, observe, + validate, ) from .volumenamingstrategy import default_format_volume_name @@ -142,6 +145,57 @@ def _container_image_changed(self, change): """ ) + image_whitelist = Union([Dict(), List()], + config=True, + help=""" + List or dict of images that users can run. + + If specified, users will be presented with a form + from which they can select an image to run. + """ + ) + + @validate('image_whitelist') + def _image_whitelist_dict(self, proposal): + """cast image_whitelist to a dict + + If passing a list, cast it to a {item:item} + dict where the keys and values are the same. + """ + whitelist = proposal.value + if not isinstance(whitelist, dict): + whitelist = {item:item for item in whitelist} + return whitelist + + @default('options_form') + def _default_options_form(self): + if len(self.image_whitelist) <= 1: + # default form only when there are images to choose from + return '' + # form derived from wrapspawner.ProfileSpawner + option_t = '' + options = [ + option_t.format( + image=image, + selected='selected' if image == self.image else '' + ) + for image in self.image_whitelist + ] + return """ + + + """.format(options=options) + + def options_from_form(self, formdata): + """Turn options formdata into user_options""" + options = {} + print(formdata) + if 'image' in formdata: + options['image'] = formdata['image'][0] + return options + container_prefix = Unicode( "jupyter", config=True, @@ -476,6 +530,25 @@ def start(self, image=None, extra_create_kwargs=None, `extra_host_config` take precedence over their global counterparts. """ + # image priority: + # 1. explicit argument + # (this never happens when DockerSpawner is used directly, + # but can be used by subclasses) + # 2. user options (from spawn options form) + # 3. self.image from config + image = image or self.user_options.get('image') or self.image + if self.image_whitelist: + if image not in self.image_whitelist: + raise web.HTTPError(400, + "Image %s not in whitelist: %s" % ( + image, ', '.join(self.image_whitelist) + ) + ) + # resolve image alias to actual image name + image = self.image_whitelist[image] + # save choice in self.image + self.image = image + container = yield self.get_container() if container and self.remove_containers: self.log.warning( @@ -486,7 +559,6 @@ def start(self, image=None, extra_create_kwargs=None, container = None if container is None: - image = image or self.image if self._user_set_cmd: cmd = self.cmd else: