diff --git a/binderhub/app.py b/binderhub/app.py index 50bcd8bb8..e0b709746 100644 --- a/binderhub/app.py +++ b/binderhub/app.py @@ -21,7 +21,7 @@ import tornado.log from tornado.log import app_log import tornado.web -from traitlets import Unicode, Integer, Bool, Dict, validate, TraitError, default +from traitlets import Unicode, Integer, Bool, Dict, validate, TraitError, default, Set from traitlets.config import Application from jupyterhub.services.auth import HubOAuthCallbackHandler @@ -402,6 +402,16 @@ def _template_path_default(self): config=True, ) + query_parameter_names = Set( + trait=Unicode(), + default_value=set(), + help=""" + List of allowed names. Before launch, BinderHub checks if they exist in the query and + pass found ones with their value to spawner's user_options. + """, + config=True + ) + @staticmethod def add_url_prefix(prefix, handlers): """add a url prefix to handlers""" @@ -512,7 +522,8 @@ def initialize(self, *args, **kwargs): 'executor': self.executor, 'auth_enabled': self.auth_enabled, 'use_named_servers': self.use_named_servers, - 'event_log': self.event_log + 'event_log': self.event_log, + 'query_parameter_names': self.query_parameter_names }) if self.auth_enabled: self.tornado_settings['cookie_secret'] = os.urandom(32) diff --git a/binderhub/builder.py b/binderhub/builder.py index e7f6c17ca..329f59955 100644 --- a/binderhub/builder.py +++ b/binderhub/builder.py @@ -470,6 +470,22 @@ async def launch(self, kube): log("Launching pod for %s: %s other pods running this repo (%s total)", self.repo_url, matching_pods, total_pods) + # get query parameters + user_options = {} + options_message = [] + for name in self.settings['query_parameter_names']: + value = self.get_query_argument(name, None) + if value is not None: + user_options[name] = value + m = f"Passing option {name} with value {value} to spawner" + app_log.info(m) + options_message.append(m) + if options_message: + await self.emit({ + 'phase': 'launching', + 'message': '\n'.join(options_message)+'\n', + }) + await self.emit({ 'phase': 'launching', 'message': 'Launching server...\n', @@ -494,7 +510,8 @@ async def launch(self, kube): server_name = '' try: server_info = await launcher.launch(image=self.image_name, username=username, - server_name=server_name, repo_url=self.repo_url) + server_name=server_name, repo_url=self.repo_url, + user_options=user_options) LAUNCH_TIME.labels( status='success', retries=i, ).observe(time.perf_counter() - launch_starttime) diff --git a/binderhub/launcher.py b/binderhub/launcher.py index ce0b8e70a..033e30a35 100644 --- a/binderhub/launcher.py +++ b/binderhub/launcher.py @@ -117,16 +117,14 @@ def unique_name_from_repo(self, repo_url): # add a random suffix to avoid collisions for users on the same image return '{}-{}'.format(prefix, ''.join(random.choices(SUFFIX_CHARS, k=SUFFIX_LENGTH))) - async def launch(self, image, username, server_name='', repo_url=''): + async def launch(self, image, username, server_name='', repo_url='', user_options=None): """Launch a server for a given image - creates a temporary user on the Hub if authentication is not enabled - - spawns a server for temporary/authenticated user + - spawns a server for temporary/authenticated user with user options - generates a token - returns a dict containing: - `url`: the URL of the server - - `image`: image spec - - `repo_url`: the url of the repo - `token`: the token for the server """ # TODO: validate the image argument? @@ -153,10 +151,13 @@ async def launch(self, image, username, server_name='', repo_url=''): raise web.HTTPError(409, "User %s already has a running server." % username) # data to be passed into spawner's user_options during launch - # and also to be returned into 'ready' state - data = {'image': image, - 'repo_url': repo_url, - 'token': base64.urlsafe_b64encode(uuid.uuid4().bytes).decode('ascii').rstrip('=\n')} + _user_options = {'image': image, + 'repo_url': repo_url, + 'token': base64.urlsafe_b64encode(uuid.uuid4().bytes).decode('ascii').rstrip('=\n')} + if user_options is None: + user_options = _user_options + else: + user_options.update(_user_options) # server name to be used in logs _server_name = " {}".format(server_name) if server_name else '' @@ -167,7 +168,7 @@ async def launch(self, image, username, server_name='', repo_url=''): resp = await self.api_request( 'users/{}/servers/{}'.format(username, server_name), method='POST', - body=json.dumps(data).encode('utf8'), + body=json.dumps(user_options).encode('utf8'), ) if resp.code == 202: # Server hasn't actually started yet @@ -196,5 +197,7 @@ async def launch(self, image, username, server_name='', repo_url=''): format(_server_name, username, e, body)) raise web.HTTPError(500, "Failed to launch image %s" % image) - data['url'] = self.hub_url + 'user/%s/%s' % (username, server_name) + # data to be returned into 'ready' state + data = {'url': self.hub_url + 'user/%s/%s' % (username, server_name), + 'token': user_options['token']} return data diff --git a/binderhub/static/js/index.js b/binderhub/static/js/index.js index 12c6d95ae..64f8c30c6 100644 --- a/binderhub/static/js/index.js +++ b/binderhub/static/js/index.js @@ -76,6 +76,7 @@ function updateRepoText() { function getBuildFormValues() { var providerPrefix = $('#provider_prefix').val().trim(); + var userOptions = $('#user_options').val().trim(); var repo = $('#repository').val().trim(); if (providerPrefix !== 'git') { repo = repo.replace(/^(https?:\/\/)?github.com\//, ''); @@ -92,7 +93,8 @@ function getBuildFormValues() { var ref = $('#ref').val().trim() || 'master'; var path = $('#filepath').val().trim(); return {'providerPrefix': providerPrefix, 'repo': repo, - 'ref': ref, 'path': path, 'pathType': getPathType()} + 'ref': ref, 'path': path, 'pathType': getPathType(), + 'userOptions': userOptions} } function updateUrls(formValues) { @@ -121,7 +123,7 @@ function updateUrls(formValues) { } } -function build(providerSpec, log, path, pathType) { +function build(providerSpec, log, path, pathType, userOptions) { update_favicon(BASE_URL + "favicon_building.ico"); // split provider prefix off of providerSpec var spec = providerSpec.slice(providerSpec.indexOf('/') + 1); @@ -139,7 +141,7 @@ function build(providerSpec, log, path, pathType) { $('.on-build').removeClass('hidden'); - var image = new BinderImage(providerSpec); + var image = new BinderImage(); image.onStateChange('*', function(oldState, newState, data) { if (data.message !== undefined) { @@ -195,7 +197,8 @@ function build(providerSpec, log, path, pathType) { image.launch(data.url, data.token, path, pathType); }); - image.fetch(); + var apiUrl = BASE_URL + "build/" + providerSpec + userOptions; + image.fetch(apiUrl); return image; } @@ -288,7 +291,8 @@ function indexMain() { formValues.providerPrefix + '/' + formValues.repo + '/' + formValues.ref, log, formValues.path, - formValues.pathType + formValues.pathType, + formValues.userOptions ); return false; }); @@ -310,7 +314,8 @@ function loadingMain(providerSpec) { pathType = 'file'; } } - build(providerSpec, log, path, pathType); + var userOptions = window.location.search; + build(providerSpec, log, path, pathType, userOptions); return false; } diff --git a/binderhub/static/js/src/image.js b/binderhub/static/js/src/image.js index f86f6a1ea..04274f20a 100644 --- a/binderhub/static/js/src/image.js +++ b/binderhub/static/js/src/image.js @@ -1,13 +1,9 @@ -var BASE_URL = $("#base-url").data().url; - -export default function BinderImage(providerSpec) { - this.providerSpec = providerSpec; +export default function BinderImage() { this.callbacks = {}; this.state = null; } -BinderImage.prototype.fetch = function() { - var apiUrl = BASE_URL + "build/" + this.providerSpec; +BinderImage.prototype.fetch = function(apiUrl) { this.eventSource = new EventSource(apiUrl); var that = this; this.eventSource.onerror = function(err) { diff --git a/binderhub/templates/index.html b/binderhub/templates/index.html index 48bb074b5..2b3066241 100644 --- a/binderhub/templates/index.html +++ b/binderhub/templates/index.html @@ -24,6 +24,7 @@