Best way to inject per user config for JuptyerHub

Hi everyone,

I’ve recently taken a fairly deep dive into the world of Jupyterhub running on AWS. I’m using the custom field on the JupyterHub k8 helm chart to specify a list of users where each additional user (or the group or section they belong to) can override or append to any of the attributes for KubeSpawner. My specification works great except for finding the ideal place to inject these new attributes. The best way to think of what I’m doing is implementing the same thing that happens in the kuberspawner_override field in a different profile on the profile list.

I’ve tried a bunch of things. First, I just used c.KubeSpawner.pre_spawn_hook to inject my code which works fine as long as I don’t want to override profile_list, which I do. So, then I made a custom options form function and set c.KubeSpawner.options_form = custom_options_form. However, this is called every time the options form is displayed which leads to a weird edge case if someone displays the options form and then doesn’t spawn a server. The problem is that I append to things (instead of just overwriting), so when I append, the appending happens twice if the person goes back to the options form before launching a server. This causes everything to break until the hub is restarted.

I tried to use the pre_spawn_start function for the authenticator, but just writing a custom function called custom_pre_spawn_start(user, spawner) and then setting c.Authenticator.pre_spawn_start = custom_pre_spawn_start didn’t do anything at all. From googling around, it seems like this is not the right way to use pre_spawn_start, but I can’t figure out the right way in an authenticator agnostic environment (this JupyterHub config is intended to be used with mutiple authenticators depending on when and where it is deployed).

What I really am looking for is a per user hook that only fires once and runs before the options form is set. I feel like per user resources is a pretty common use case, so I’m hoping that others have run into this as well and have ideas. I’m happy to share more about my particular config if it’s helpful.

Thanks everyone!

1 Like

Only run once can be handled with something that runs every time, but sets a flag on first run so it doesn’t need to go again, or some other check to make sure that multiple runs are idempotent. For example:

def construct_options_form(spawner):
    if not getattr(spawner, '_loaded_extra_stuff', False):
        # only run this bit once
        spawner._loaded_extra_stuff = True
        spawner.profile_list.append('...')
    return '...'

But if it’s just specifying additional items in the profile list in a way that’s stable but user-sensitive, you can make profile_list itself the callable:

common_profiles = [
    {'display_name': 'default',
    'default': True,
    ...,
]

def per_user_profiles(spawner):
    return common_profiles + [per_user_profile(spawner.user)]

c.KubeSpawner.profile_list = per_user_profiles

Most kubespawner traitlets can be callables like this, and all traitlets can be produced by methods with a subclassed Spawner by using traitlets dynamic defaults feature.

2 Likes

Thanks so much for your reply @minrk! It’s definitely very helpful, and it is more or less the solution I had already implemented. I ended up setting spawner.environment['ENVIRONMENT_MODIFIED'] = True when modifying the environment, and then checking it on subsequent calls. I didn’t just have a variable like spawner._loaded_extra_stuff becuase I didn’t want to subclass the spawner.

However, this feels really “hacky”, and I feel like this is probably a pretty common use case (modifying environments on a per user basis). The problem is that I am trying to stick as closely as possible to the existing jupyterhub-k8 helm chart, so I’m trying to avoid implementing a custom authenticator or spawner since either option will require maintaining a separate hub image or replicating a bunch of the spawner/authenticator config code in the hub.extraConfig block. This makes my custom implementation a lot less maintainable over the long term.

I also didn’t know that most kubespawner traitlets could be callable. That is super interesting, but I don’t think it is quite what I’m looking for.

Is there any plans to implement a general hook anywhere in the JupyterHub stack at user sign on in order to do user specific config? Again, I understand that the authenticator pre_spawn_start can do this, but best I can tell, you have to subclass the authenticator and implement this in a custom subclass. There isn’t any way to set this directly in a spawner agnostic framework?

Thanks again for your reply!