Background
If you want to customize something based on information about the user logged in and about to start a server, perhaps based on custom python logic, you can!
For example, perhaps you have information about the user being part of a group with access to GPUs and want that to influence some spawn options and server configurations?
This post is about how you could go about that practically.
Problem 1 - Adjust presented server options based on user
What if you want to present different server options to different users, based on some property of the user? Just setting c.KubeSpawner.profile_list
/ singleuser.profileList
won’t do because then all users would get the same options.
The solution to this is to update c.KubeSpawner.options_form
with a custom function that first updates the the_users_spawner_instance.profile_list
based on logic that involve the user information.
Common section for examples below
The examples below assume we can request a “scope” called “gpu_access”, which is supposed to return a “claim” with the same name as a true or false value.
# This snipped assumes a OAuthenticator based Authenticator, and the scopes will
# vary and this is just an example.
#
# - openid is common for OpenID Connect based authentication
# - profile, email are both commonly available scopes to request
# - gpu_access is entirely custom
#
c.OAuthenticator.scopes = ["openid", "profile", "email", "gpu_access"]
Solution to problem 1
To present JupyterHub server spawn options based on the user
# To adjust the spawn options presented to the user, we must create a custom
# options_form function, and this example demonstrates how!
#
#
#
# profile_list (KubeSpawner class) can be configured as a convenience to
# generate set HTML for the options_form configuration (Spawner class).
#
# If options_form is set (or indirectly set through profile_list), it is the
# HTML that users are presented with when users have signed in and want to start
# a server.
#
# While options_form is allowed to be a HTML string, it can also be a callable
# function, that when called generates HTML. If a callable function return a
# falsy value, no form will be rendered.
#
# In this custom options_form function, we will make a decision based on user
# information, update profile_list, and rely on the profile_list logic to render
# the HTML for us.
#
async def custom_options_form(spawner):
# See the pre_spawn_hook example for more ways to get information about the
# user
auth_state = await spawner.user.get_auth_state()
user_details = auth_state["oauth_user"]
gpu_access = user_details.get("gpu_access", False)
# Declare the common profile list for all users
spawner.profile_list = [
{
'display_name': 'CPU server',
'slug': 'cpu',
'default': True,
},
]
# Dynamically update profile_list based on user
if gpu_access:
spawner.log.info(f"GPU access options added for {username}.")
spawner.profile_list.extend(
[
{
'display_name': 'GPU server',
'slug': 'gpu',
'kubespawner_override': {
'image': 'training/datascience:my_tag',
},
},
]
)
# Let KubeSpawner inspect profile_list and decide what to return, it
# will return a falsy blank string if there is no profile_list,
# which makes no options form be presented.
#
# ref: https://github.com/jupyterhub/kubespawner/blob/37a80abb0a6c826e5c118a068fa1cf2725738038/kubespawner/spawner.py#L1885-L1935
return spawner._options_form_default()
# Don't forget to ask KubeSpawner to make use of this custom hook
c.KubeSpawner.options_form = custom_options_form
Problem 2 - Adjust spawned server config based on user and/or user’s chosen option
What if you wanted to add a label, environment variable, container etc. or similar to a user pod based on the user, the users “auth state”, and/or the chosen profile among the profile_list
profiles? Setting c.KubeSpawner.extra_labels
or similar would not cut it, because it would influence all users, not just a specific user.
The solution to this is to update c.KubeSpawner.pre_spawn_hook
with a custom function that updates the the_users_spawner_instance.<some option>
based on logic that involve the user information.
Solution to problem 2
To adjust the server based on the user, the users “auth state”, and/or the chosen profile.
async def custom_pre_spawn_hook(spawner):
# Here are examples of information available to us:
#
# 1. Info about the user from the JupyterHub User object
#
username = spawner.user.name
# ... more User object properties are available, for more information, see:
# https://jupyterhub.readthedocs.io/en/latest/_static/rest-api/index.html#/definitions/User
# 2. Info about the user's profile if profile_list was configured
#
# This value will be either the "slug" or "display_name". In our example,
# "cpu" or "gpu".
#
chosen_profile = spawner.user_options.get("profile", "")
# 3. Info about the user from the user's authentication state
#
# The authentication state must be enabled for this to be accessible, so
# set hub.config.Authenticator.enable_auth_state to true.
#
# Depending on what authenticator you use and how it is configured, you
# can get access to different things. By setting an OAuthenticator based
# class "scope" to include "email", you should have email "claim"
# available. You request scopes, and can be returned claims.
#
auth_state = await spawner.user.get_auth_state()
user_details = auth_state["oauth_user"]
gpu_access = user_details.get("gpu_access", False)
# Here are examples on how to update settings based on user information:
#
# 1. Setting a label
#
# With .update on this dict, we don't remove all other extra_labels
#
spawner.extra_labels.update(
{
"sundellopensource.se/gpu-access": str.lower(str(gpu_access)),
}
)
# 2. Add an init_container
#
# With .insert on this list, we don't remove all other init_containers
#
init_container_for_gpu_users = {} # FIXME: not a complete example
spawner.init_containers.insert(0, init_container_for_gpu_users)
# Don't forget to ask KubeSpawner to make use of this custom hook
c.KubeSpawner.pre_spawn_hook = custom_pre_spawn_hook