Skip to content
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

Merged
merged 17 commits into from
Apr 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 140 additions & 5 deletions kubespawner/spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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!

Expand Down Expand Up @@ -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']
}
]

"""
)

Expand Down Expand Up @@ -800,6 +801,88 @@ def _hub_connect_port_default(self):
"""
)

profile_form_template = Unicode(
"""
<label for="profile">Please select a profile to launch</label>
<select class="form-control" name="profile" required autofocus>
{% for profile in profile_list %}
<option {% if profile.default %}selected{% endif %} value="{{ loop.index0 }}">{{ profile.display_name }}</option>
{% endfor %}
</select>
""",
config=True,
help="""
Jinja2 template for constructing profile list shown to user.

Used when `profile_list` is set.

The contents of `profile_list` are passed in to the template.
This should be used to construct the contents of a HTML form. When
posted, this form is expected to have an item with name `profile` and
the value the index of the profile in `profile_list`.
"""
)

profile_list = List(
trait=Dict(),
default_value=None,
minlen=0,
config=True,
help="""
List of profiles to offer for selection by the user.

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.
- 'default': (optional Bool) True if this is the default selected option

Example::

c.KubeSpawner.profile_list = [
{
'display_name': 'Training Env - Python',
'default': True,
'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)
Expand Down Expand Up @@ -1127,3 +1210,55 @@ def get_args(self):
args[i] = '--hub-api-url="%s"' % (self.accessible_hub_api_url)
break
return args

def _options_form_default(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add docstrings for this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
'' when no `profile_list` has been defined
The rendered template (using jinja2) when `profile_list` is defined.
'''
if not self.profile_list:
return ''
profile_form_template = Environment(loader=BaseLoader).from_string(self.profile_form_template)
return profile_form_template.render(profile_list=self.profile_list)

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:
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
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
'pyYAML',
'kubernetes==4.*',
'escapism',
'jinja2',
],
setup_requires=['pytest-runner'],
tests_require=['pytest'],
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
minversion = 1.6
skipsdist = True

envlist = py34
envlist = py34,py35,py36

[flake8]
max-line-length = 100