Set KubeSpawner Environment During Server Creation

I followed the Zero to Hero k8s guide and have a working deployment on my kubernetes cluster, but I’ve run into a problem that has me stumped.

Using JupyterHub’s Rest API, I’m attempting to create a named server and set an environment variable in the singeluser pod. I’m aware this can be achieved through the values.yaml file, but this environment variable is dynamic, and can only be set at runtime.

According to my reading of the documentation, it seems like I should be able to set environment variables by providing a JSON body on the POST request to create the initial server. So I tried to do something like:

import httpx

client = httpx.Client(headers={"Authorization": "token secret-token"})
url = "http://localhost:8001/hub/api/users/username/servers/servername"
client.post(url, json={"environment": {"FOO": "BAR"})

The server comes up as expected, but the environment variable FOO is not set as I expected it would be. The user_options are set however:

client.get(url).json()["servers"]["servername"]

Returns:

{
  "last_activity": "2022-09-14T02:12:36.082000Z",
  "name": "servername",
  "pending": null,
  "progress_url": "/hub/api/users/username/servers/servername/progress",
  "ready": true,
  "started": "2022-09-14T01:29:14.042675Z",
  "state": {
    "pod_name": "jupyter-username--servername"
  },
  "stopped": false,
  "url": "/user/username/servername/",
  "user_options": {
    "environment": {
      "FOO": "BAR"
    }
  }
}

Here is my version info on the Hub itself:

{
  "authenticator": {
    "class": "jupyterhub.auth.DummyAuthenticator",
    "version": "3.0.0"
  },
  "python": "3.9.14 (main, Sep  7 2022, 23:09:15) \n[GCC 10.2.1 20210110]",
  "spawner": {
    "class": "kubespawner.spawner.KubeSpawner",
    "version": "4.2.0"
  },
  "sys_executable": "/usr/local/bin/python",
  "version": "3.0.0"
}

I’m not sure if what I’m trying to do should work or not, but it feels like this is a common thing people would want to do, so I’m sure there’s a way to do it I’m overlooking. Thanks in advance for any help.

For security reasons, user_options is not a pass through to Spawner attributes. It only sets the user_options dict. To expose a new option via user_options, you have to implement that in Spawner.load_user_options to set e.g. self.environment.update(user_options[“environment”])

2 Likes

Awesome. I implemented my own spawner and had to push a separate hub image but it worked. Is that the preferred way to do it?

Thanks for the fast response.

Pretty much. You can define custom Spawner subclasses with this small level of customization in jupyterhub_config.py:

# jupyterhub_config.py

from somespawner import SomeSpawner


class MySpawner(SomeSpawner):
    def start(self):
        # expose additional user options
        if self.user_options.get("attr") is not None:
            self.attr = self.user_options["attr"]
        return super().start()

c.JupyterHub.spawner_class = MySpawner

without needing to make a package and install it separately. We’re currently working on ways to better specify this kind of thing to handle more cases with declarative config, but right now you usually have to write at least a custom hook function, if not a thin subclass.