Merge pull request #3013 from twalcari/feature/spawn_query_arguments

This commit is contained in:
Min RK
2020-05-19 15:06:17 +02:00
committed by GitHub
4 changed files with 106 additions and 1 deletions

View File

@@ -175,11 +175,41 @@ class SpawnHandler(BaseHandler):
auth_state = await user.get_auth_state()
await spawner.run_auth_state_hook(auth_state)
# Try to start server directly when query arguments are passed.
error_message = ''
query_options = {}
for key, byte_list in self.request.query_arguments.items():
query_options[key] = [bs.decode('utf8') for bs in byte_list]
# 'next' is reserved argument for redirect after spawn
query_options.pop('next', None)
if len(query_options) > 0:
try:
self.log.debug(
"Triggering spawn with supplied query arguments for %s",
spawner._log_name,
)
options = await maybe_future(spawner.options_from_query(query_options))
pending_url = self._get_pending_url(user, server_name)
return await self._wrap_spawn_single_user(
user, server_name, spawner, pending_url, options
)
except Exception as e:
self.log.error(
"Failed to spawn single-user server with query arguments",
exc_info=True,
)
error_message = str(e)
# fallback to behavior without failing query arguments
spawner_options_form = await spawner.get_options_form()
if spawner_options_form:
self.log.debug("Serving options form for %s", spawner._log_name)
form = await self._render_form(
for_user=user, spawner_options_form=spawner_options_form
for_user=user,
spawner_options_form=spawner_options_form,
message=error_message,
)
self.finish(form)
else:

View File

@@ -384,6 +384,37 @@ class Spawner(LoggingConfigurable):
"""
return form_data
def options_from_query(self, query_data):
"""Interpret query arguments passed to /spawn
Query arguments will always arrive as a dict of unicode strings.
Override this function to understand single-values, numbers, etc.
By default, options_from_form is called from this function. You can however override
this function if you need to process the query arguments differently.
This should coerce form data into the structure expected by self.user_options,
which must be a dict, and should be JSON-serializeable,
though it can contain bytes in addition to standard JSON data types.
This method should not have any side effects.
Any handling of `user_options` should be done in `.start()`
to ensure consistent behavior across servers
spawned via the API and form submission page.
Instances will receive this data on self.user_options, after passing through this function,
prior to `Spawner.start`.
.. versionadded:: 1.2
user_options are persisted in the JupyterHub database to be reused
on subsequent spawns if no options are given.
user_options is serialized to JSON as part of this persistence
(with additional support for bytes in case of uploaded file data),
and any non-bytes non-jsonable values will be replaced with None
if the user_options are re-used.
"""
return self.options_from_form(query_data)
user_options = Dict(
help="""
Dict of user specified options for the user's spawned instance of a single-user server.

View File

@@ -173,6 +173,9 @@ class FormSpawner(MockSpawner):
options['energy'] = form_data['energy'][0]
if 'hello_file' in form_data:
options['hello'] = form_data['hello_file'][0]
if 'illegal_argument' in form_data:
raise ValueError("You are not allowed to specify 'illegal_argument'")
return options

View File

@@ -255,6 +255,47 @@ async def test_spawn_page_admin(app, admin_access):
assert "Spawning server for {}".format(u.name) in r.text
async def test_spawn_with_query_arguments(app):
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
base_url = ujoin(public_host(app), app.hub.base_url)
cookies = await app.login_user('jones')
orm_u = orm.User.find(app.db, 'jones')
u = app.users[orm_u]
await u.stop()
next_url = ujoin(app.base_url, 'user/jones/tree')
r = await async_requests.get(
url_concat(
ujoin(base_url, 'spawn'), {'next': next_url, 'energy': '510keV'},
),
cookies=cookies,
)
r.raise_for_status()
assert r.history
assert u.spawner.user_options == {
'energy': '510keV',
'notspecified': 5,
}
async def test_spawn_with_query_arguments_error(app):
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
base_url = ujoin(public_host(app), app.hub.base_url)
cookies = await app.login_user('jones')
orm_u = orm.User.find(app.db, 'jones')
u = app.users[orm_u]
await u.stop()
next_url = ujoin(app.base_url, 'user/jones/tree')
r = await async_requests.get(
url_concat(
ujoin(base_url, 'spawn'),
{'next': next_url, 'energy': '510keV', 'illegal_argument': '42'},
),
cookies=cookies,
)
r.raise_for_status()
assert "You are not allowed to specify " in r.text
async def test_spawn_form(app):
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
base_url = ujoin(public_host(app), app.hub.base_url)