Hi,
for various purposes at our university, we would like to offer people spawning a notebook a drop-down menu in the form that would list existing PVCs in their namespace.
I’m trying to implement this simple function in the custom spawner as we have a lot of stuff modified. I tried implementation in JavaScript but that failed because the @kubernetes/client-node library is meant to be server side and it is not loaded when form is active. I thought I might create a new bundle with browsify but because the library is server-side, it would not work.
Then I thought I might implement the same function in Python in a custom DockerImageChooser where the python function is a class function, list of PVCs is a variable and the function is called as c.DockerImageChooser.pvcnames = get_persistent_homes()
.
See relevant config part below:
hub:
extraConfig:
form-custom-class: |
from traitlets import default, Unicode, List
from tornado import gen
from kubespawner import KubeSpawner
from kubernetes_asyncio import config, client
import re
import asyncio
class DockerImageChooser(KubeSpawner):
pvcsnames = List(
trait = Unicode(),
default_value = ["viktorias-home-nonex"],
config = True,
help = "Phomes."
)
form_template = Unicode("""
<script>
function customImageCheck(that) {{
if (that.value == "custom") {{
document.getElementById("custom").style.display = "block";
}} else {{
document.getElementById("custom").style.display = "none";
}}
}}
...
<label for="phselection">Select persistent home:</label>
<select class="data-list-input" name="phselection" required="required" style="width:190px;" id="sphid" onchange="getHomesFromKubernetes();">
<option value="new">New</option>
<option value="existing">Existing</option>
</select>
<div id="pvcnames" style="display: none;">
<label for="phid">Select persistent home:</label>
<select class="form-control" name="phname" id="phid" required autofocus>
{option_template}
</select>
</div>
...""",
config = True, help = "Form template."
)
option_template = Unicode("""
<option value="{item}">{item}</option>""",
config = True, help = "Template for html form options."
)
@default('options_form')
async def _options_form(self):
"""Return the form with the drop-down menu."""
config.load_incluster_config()
username = "viktorias"
namespace = "jupyterhub-" + username
k8s_client = client.ApiClient()
v1 = client.CoreV1Api(k8s_client)
phomes = []
pvcs = await v1.list_namespaced_persistent_volume_claim(namespace=namespace)
for pvc in pvcs.items:
if re.match(username + "-home-", pvc.metadata.name):
phomes.append(pvc.metadata.name)
options = ''.join([
self.option_template.format(item=p) for p in phomes
])
return self.form_template.format(option_template=options)
def options_from_form(self, formdata):
"""Parse the submitted form data and turn it into the correct
structures for self.user_options."""
options = {}
default_image = "jupyter/minimal-notebook:67b8fb91f950"
default_home = self.homes[0]
...
return options
form-homes: |
c.JupyterHub.spawner_class = DockerImageChooser
c.DockerImageChooser.pvcsnames = DockerImageChooser.get_p_homes()
I have been working on this for a couple of days now as there were a couple of problems:
- firstly I wanted to implement the function in JavaScript similarly as
customImageCheck
which failed on not being able to load the library - usage of kubernetes_asyncio in Python … I could not get
list_namespaced_persistent_volume_claim
to work as it should be awaited
I finally got the right code in order to spawn the hub container but since then, my attempts later just pass the authentication but right after, receive 500 Internal Server Error
with
traitlets.traitlets.TraitError: The 'options_form' trait of a DockerImageChooser instance expected a unicode string or a callable, not the coroutine <coroutine object DockerImageChooser._options_form at 0x7fd99dd7dac0>.
I tried:
- Using
async with
@default('options_form')
async def _options_form(self):
"""Return the form with the drop-down menu."""
config.load_incluster_config()
username = "viktorias"
namespace = "jupyterhub-" + username
phomes=[]
async with client.ApiClient() as api_client:
v1 = client.CoreV1Api(k8s_client)
pvcs = await v1.list_namespaced_persistent_volume_claim(namespace=namespace)
await asyncio.sleep(1)
for pvc in pvcs.items:
if re.match(username + "-home-", pvc.metadata.name):
phomes.append(pvc.metadata.name)
options = ''.join([
self.option_template.format(item=p) for p in phomes
])
return self.form_template.format(option_template=options)
- Dividing the code into a separate function that was annotated with
@classmethod
, creating a variable in the class and then callingc.DockerImageChooser.pvcnames = get_persistent_homes()
which would fill the variable
@classmethod
async def get_ph(self, phomes):
config.load_incluster_config()
username = "viktorias"
namespace = "jupyterhub-" + username
k8s_client = client.ApiClient()
v1 = client.CoreV1Api(k8s_client)
pvcs = await v1.list_namespaced_persistent_volume_claim(namespace=namespace)
for pvc in pvcs.items:
if re.match(username + "-home-", pvc.metadata.name):
phomes.append(pvc.metadata.name)
@classmethod
def get_p_homes(self):
pns = []
asyncio.create_task(self.get_ph(pns))
return pns
option_template = Unicode("""
<option value="{item}">{item}</option>""",
config = True, help = "Template for html form options."
)
@default('options_form')
async def _options_form(self):
"""Return the form with the drop-down menu."""
options = ''.join([
self.option_template.format(item=p) for p in self.pvcsnames
])
return self.form_template.format(option_template=options)
I know that defining _options_form
works because I had a very similar (nearly identical) implementation of rendering a predefined list which worked.
extraConfig:
form-custom-class: |
from traitlets import default, Unicode, List
from tornado import gen
from kubespawner import KubeSpawner
class DockerImageChooser(KubeSpawner):
homes = List(
trait = Unicode(),
default_value = ['brno3-cerit'],
minlen = 1,
config = True,
help = "Mountpoints for homes."
)
...
<div id="customHome" style="display: none;">
<label for "home">Select home:</label>
<select class="form-control" name="home" required autofocus>
{option_template}
</select>
</div>
...
option_template = Unicode("""
<option value="{home}">{home}</option>""",
config = True, help = "Template for html form options."
)
@default('options_form')
def _options_form(self):
"""Return the form with the drop-down menu."""
options = ''.join([
self.option_template.format(home=h) for h in self.homes
])
return self.form_template.format(option_template=options)
...
form-homes: |
c.JupyterHub.spawner_class = DockerImageChooser
c.DockerImageChooser.homes = [
"storage1", "storage2", ...
]
I had to remove this part for now because I also can’t seem to get 2 templates working.
Therefore, my final qustions:
- how can I query Kubernetes API (assuming serviceAccounToken is mounted) and interpolating the output as dropdown menu in the spawn form?
- how can I define 2 lists to interpolate in the form?
Thanks a lot!!
Viktoria