-
Notifications
You must be signed in to change notification settings - Fork 304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add kube profile spawner #137
Changes from 13 commits
4c92082
d28fb2b
a29ad4f
3b338ff
63099aa
fceee07
b5e0aec
241d47b
ad2b415
7fbe23c
0281b50
e076b50
8d0a444
b3ea43b
8344d1a
02bb801
efb48ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
from kubernetes.client.rest import ApiException | ||
from kubernetes import client | ||
import escapism | ||
from jinja2 import Environment, BaseLoader | ||
|
||
from .clients import shared_client | ||
from kubespawner.traitlets import Callable | ||
|
@@ -630,7 +631,7 @@ def _hub_connect_port_default(self): | |
List of access modes the user has for the pvc. | ||
|
||
The access modes are: | ||
|
||
- `ReadWriteOnce` – the volume can be mounted as read-write by a single node | ||
- `ReadOnlyMany` – the volume can be mounted read-only by many nodes | ||
- `ReadWriteMany` – the volume can be mounted as read-write by many nodes | ||
|
@@ -649,7 +650,7 @@ def _hub_connect_port_default(self): | |
The keys is name of hooks and there are only two hooks, postStart and preStop. | ||
The values are handler of hook which executes by Kubernetes management system when hook is called. | ||
|
||
Below is an sample copied from | ||
Below is an sample copied from | ||
`Kubernetes doc <https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/>`_ :: | ||
|
||
lifecycle: | ||
|
@@ -688,7 +689,7 @@ def _hub_connect_port_default(self): | |
add: | ||
- NET_ADMIN | ||
|
||
|
||
See https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ for more | ||
info on what init containers are and why you might want to use them! | ||
|
||
|
@@ -754,15 +755,15 @@ def _hub_connect_port_default(self): | |
which follows spec at https://v1-6.docs.kubernetes.io/docs/api-reference/v1.6/#container-v1-core. | ||
|
||
One usage is setting crontab in a container to clean sensitive data with configuration below:: | ||
|
||
[ | ||
{ | ||
'name': 'crontab', | ||
'image': 'supercronic', | ||
'command': ['/usr/local/bin/supercronic', '/etc/crontab'] | ||
} | ||
] | ||
|
||
""" | ||
) | ||
|
||
|
@@ -800,6 +801,92 @@ def _hub_connect_port_default(self): | |
""" | ||
) | ||
|
||
form_template = Unicode( | ||
"""<label for="profile">Please select a profile for your environment:</label> | ||
<select class="form-control" name="profile" required autofocus> | ||
{{ inputs }} | ||
</select> | ||
""", | ||
config = True, | ||
help = """Jinja2 template to use to construct options_form text. {inputs} is replaced with | ||
the result of formatting inputs against each item in the profiles list.""" | ||
) | ||
|
||
inputs = Unicode(""" | ||
<option value="{{ key }}" {{ first }} >{{ display }}</option>""", | ||
config = True, | ||
help = """Jinja template to construct {inputs} in form_template. This text will be formatted | ||
against each item in the profiles list, in order, using the following key names: | ||
( display, key, type ) for the first three items in the tuple, and additionally | ||
first = "checked" (taken from first_input) for the first item in the list, so that | ||
the first item starts selected.""" | ||
) | ||
|
||
first_input = Unicode('selected', | ||
config=True, | ||
help="Text to substitute as {first} in inputs" | ||
) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unintended whitespaces? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be resolved |
||
options_form = Unicode() | ||
|
||
profile_list = List( | ||
trait = Dict(), | ||
default_value = None, | ||
minlen = 0, | ||
config = True, | ||
help = """ | ||
List of profiles to offer for selection by the user. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. <3 this is great! |
||
|
||
Signature is: List(Dict()), where each item is a dictionary that has two keys: | ||
- 'display_name': the human readable display name (should be HTML safe) | ||
- 'kubespawner_override': a dictionary with overrides to apply to the KubeSpawner | ||
settings. Each value can be either the final value to change or a callable that | ||
take the `KubeSpawner` instance as parameter and return the final value. | ||
|
||
Example:: | ||
|
||
c.KubeSpawner.profile_list = [ | ||
{ | ||
'display_name': 'Training Env - Python', | ||
'kubespawner_override': { | ||
'singleuser_image_spec': 'training/python:label', | ||
'cpu_limit': 1, | ||
'mem_limit': '512M', | ||
} | ||
}, { | ||
'display_name': 'Training Env - Datascience', | ||
'kubespawner_override': { | ||
'singleuser_image_spec': 'training/datascience:label', | ||
'cpu_limit': 4, | ||
'mem_limit': '8G', | ||
} | ||
}, { | ||
'display_name': 'DataScience - Small instance', | ||
'kubespawner_override': { | ||
'singleuser_image_spec': 'datascience/small:label', | ||
'cpu_limit': 10, | ||
'mem_limit': '16G', | ||
} | ||
}, { | ||
'display_name': 'DataScience - Medium instance', | ||
'kubespawner_override': { | ||
'singleuser_image_spec': 'datascience/medium:label', | ||
'cpu_limit': 48, | ||
'mem_limit': '96G', | ||
} | ||
}, { | ||
'display_name': 'DataScience - Medium instance (GPUx2)', | ||
'kubespawner_override': { | ||
'singleuser_image_spec': 'datascience/medium:label', | ||
'cpu_limit': 48, | ||
'mem_limit': '96G', | ||
'extra_resource_guarantees': {"nvidia.com/gpu": "2"}, | ||
} | ||
} | ||
] | ||
""" | ||
) | ||
|
||
def _expand_user_properties(self, template): | ||
# Make sure username and servername match the restrictions for DNS labels | ||
safe_chars = set(string.ascii_lowercase + string.digits) | ||
|
@@ -1127,3 +1214,67 @@ def get_args(self): | |
args[i] = '--hub-api-url="%s"' % (self.accessible_hub_api_url) | ||
break | ||
return args | ||
|
||
def _options_form_default(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add docstrings for this method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure ! By the way, do you know how the magic work behind the [_]options_form*? I have no idea how it get called (by tornado?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a JupyterHub feature, see https://github.com/jupyterhub/jupyterhub/blob/a58bea6d933b7bc0bcfe4b08dfff4f31dd2b5909/jupyterhub/spawner.py#L271 and related. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes but the call is for the function self.options_form and ultimately the __default is called... |
||
''' | ||
Build the form template according to the `profile_list` setting. | ||
|
||
Returns: | ||
None when no `profile_list` has been defined | ||
The rendered template (using jinja2) when `profile_list` is defined. | ||
''' | ||
if not self.profile_list: | ||
return | ||
temp_keys = [ | ||
{ | ||
'display': p['display_name'], | ||
'key': i, | ||
'first': '', | ||
} for i, p in enumerate(self.profile_list)] | ||
temp_keys[0]['first'] = self.first_input | ||
inputs_tpl = Environment(loader=BaseLoader).from_string(self.inputs) | ||
text = ''.join([ inputs_tpl.render(**tk) for tk in temp_keys ]) | ||
rtemplate = Environment(loader=BaseLoader).from_string(self.form_template) | ||
data = { | ||
'inputs': text, | ||
} | ||
return rtemplate.render(**data) | ||
|
||
def options_from_form(self, formdata): | ||
"""get the option selected by the user on the form | ||
|
||
It actually reset the settings of kubespawner to each item found in the selected profile | ||
(`kubespawner_override`). | ||
|
||
Args: | ||
formdata: user selection returned by the form | ||
|
||
To access to the value, you can use the `get` accessor and the name of the html element, | ||
for example:: | ||
|
||
formdata.get('profile',[0]) | ||
|
||
to get the value of the form named "profile", as defined in `form_template`:: | ||
|
||
<select class="form-control" name="profile"...> | ||
</select> | ||
|
||
Returns: | ||
the selected user option | ||
""" | ||
|
||
if not self.profile_list: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add docstrings for this method? |
||
return formdata | ||
# Default to first profile if somehow none is provided | ||
selected_profile = int(formdata.get('profile',[0])[0]) | ||
options = self.profile_list[selected_profile] | ||
self.log.debug("Applying KubeSpawner override for profile '%s'", options['display_name']) | ||
kubespawner_override = options.get('kubespawner_override', {}) | ||
for k, v in kubespawner_override.items(): | ||
if callable(v): | ||
v = v(self) | ||
self.log.debug(".. overriding KubeSpawner value %s=%s (callable result)", k, v) | ||
else: | ||
self.log.debug(".. overriding KubeSpawner value %s=%s", k, v) | ||
setattr(self, k, v) | ||
return options |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
minversion = 1.6 | ||
skipsdist = True | ||
|
||
envlist = py34 | ||
envlist = py34,py35,py36 | ||
|
||
[flake8] | ||
max-line-length = 100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I wonder if we can make this be a jinja2 template instead? That gives users a lot more control than just python formatstrings. It would let us have just one 'form_template' rather than have a form_template and an inputs template.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is jinja included in kubespawner?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jinja2 is included in JupyterHub. I think the other templates use string.format because they are fairly simple. I'd like jinja2 for this one because it sets up HTML, and string.format might easily allow XSS and other issues. All the html templating in JupyterHub is jinja2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I have not tested it yet, but I have switched to jinja rendered + docstring