Skip to content

Commit 4fcb2bf

Browse files
authored
Merge pull request #143 from jtpio/tljh-docker-plugin
Move the environment service to its own TLJH plugin
2 parents b53b8a5 + bf89217 commit 4fcb2bf

File tree

14 files changed

+63
-681
lines changed

14 files changed

+63
-681
lines changed

ansible/tljh.yml

+9-2
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,25 @@
2424
changed_when: False
2525

2626
- name: Upgrade the tljh-plasmabio plugin first if it is already installed
27-
shell: "{{ tljh_prefix }}/hub/bin/pip3 install --upgrade {{ tljh_plasmabio }}"
27+
shell: "{{ tljh_prefix }}/hub/bin/pip3 install --upgrade {{ tljh_plasmabio }} {{ tljh_repo2docker }}"
2828
when: tljh_plasmabio_installed.rc == 0
2929

3030
- name: Run the TLJH installer
31-
shell: "{{ ansible_python_interpreter }} {{ tljh_installer_dest }} --no-user-env --plugin {{ tljh_plasmabio }}"
31+
shell: |
32+
{{ ansible_python_interpreter }} {{ tljh_installer_dest }} --no-user-env \
33+
--plugin {{ tljh_plasmabio }} {{ tljh_repo2docker }}
3234
# TODO: remove when --no-user-env (or equivalent) is available
3335
environment:
3436
TLJH_BOOTSTRAP_PIP_SPEC: "{{ tljh_bootstrap_pip_spec }}"
3537

3638
- name: Set the idle culler timeout to 1 hour
3739
shell: "tljh-config set services.cull.timeout 3600"
3840

41+
- name: Set the default memory and cpu limits
42+
shell: |
43+
tljh-config set limits.memory 2G
44+
tljh-config set limits.cpu 2
45+
3946
- name: Reload the hub
4047
shell: "tljh-config reload hub"
4148

ansible/vars/default.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22

33
tljh_plasmabio: git+https://github.com/plasmabio/plasmabio@master#"egg=tljh-plasmabio&subdirectory=tljh-plasmabio"
4+
tljh_repo2docker: git+https://github.com/plasmabio/tljh-repo2docker@master
45
tljh_installer_dest: /srv/tljh-installer.py
56
tljh_prefix: /opt/tljh
67
# TODO: update to upstream TLJH links when the --no-user-env (or similar) is available

dev-requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
git+https://github.com/jupyterhub/the-littlest-jupyterhub
1+
git+https://github.com/jupyterhub/the-littlest-jupyterhub
2+
git+https://github.com/plasmabio/tljh-repo2docker

docs/environments/index.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ User Environments
44
User environments are built as immutable `Docker images <https://docs.docker.com/engine/docker-overview>`_.
55
The Docker images bundle the dependencies, extensions, and predefined notebooks that should be available to all users.
66

7-
PlasmaBio uses `jupyter-repo2docker <https://repo2docker.readthedocs.io>`_ to build the images on the server.
7+
PlasmaBio relies on the `tljh-repo2docker <https://github.com/plasmabio/tljh-repo2docker>`_ plugin to manage environments.
8+
The ``tljh-repo2docker`` uses `jupyter-repo2docker <https://repo2docker.readthedocs.io>`_ to build the Docker images.
89

910
Environments can be managed by admin users by clicking on ``Environments`` in the navigation bar:
1011

jupyterhub_config.py

+10-17
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
11
"""
22
This file is only used for local development
3-
and overrides some of the default values.
3+
and overrides some of the default values from the plugin.
44
"""
55

66
import getpass
77
import os
88

9-
from tljh_plasmabio import create_pre_spawn_hook, tljh_custom_jupyterhub_config
9+
from tljh_plasmabio import tljh_custom_jupyterhub_config
10+
from tljh_repo2docker import (
11+
tljh_custom_jupyterhub_config as tljh_repo2docker_config_hook,
12+
)
1013

1114
c.JupyterHub.services = []
1215

16+
# tljh-plasmabio depends on tljh-repo2docker
17+
tljh_repo2docker_config_hook(c)
1318
tljh_custom_jupyterhub_config(c)
1419

1520
user = getpass.getuser()
1621

1722
c.Authenticator.admin_users = {user}
1823

19-
# configure volumes for local setup
20-
volumes_path = os.path.join(os.getcwd(), "volumes/user")
21-
shared_data_path = os.path.join(os.getcwd(), "volumes/data")
22-
23-
c.SystemUserSpawner.volumes = {
24-
os.path.join(
25-
os.path.dirname(__file__),
26-
"tljh-plasmabio",
27-
"tljh_plasmabio",
28-
"entrypoint",
29-
"entrypoint.sh",
30-
): "/usr/local/bin/repo2docker-entrypoint",
31-
shared_data_path: {"bind": "/srv/data", "mode": "ro"},
32-
}
33-
c.SystemUserSpawner.pre_spawn_hook = create_pre_spawn_hook(volumes_path)
24+
# configure the volumes paths for local setup
25+
c.PlasmaBioSpawner.base_path = os.path.join(os.getcwd(), "volumes/user")
26+
c.PlasmaBioSpawner.shared_data_path = os.path.join(os.getcwd(), "volumes/data")

tljh-plasmabio/MANIFEST.in

-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
recursive-include tljh_plasmabio/templates/ *
2-
recursive-include tljh_plasmabio/static/ *
32
recursive-include tljh_plasmabio/entrypoint/ *

tljh-plasmabio/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
entry_points={"tljh": ["tljh_plasmabio = tljh_plasmabio"]},
77
packages=find_packages(),
88
include_package_data=True,
9-
install_requires=["dockerspawner", "jupyter_client", "docker"],
9+
install_requires=["dockerspawner", "tljh_repo2docker"],
1010
)

tljh-plasmabio/tljh_plasmabio/__init__.py

+38-102
Original file line numberDiff line numberDiff line change
@@ -4,140 +4,76 @@
44

55
from dockerspawner import SystemUserSpawner
66
from jupyterhub.auth import PAMAuthenticator
7-
from jupyter_client.localinterfaces import public_ips
87
from tljh.hooks import hookimpl
98
from tljh.systemd import check_service_active
10-
from traitlets import default
9+
from tljh_repo2docker import TLJHDockerSpawner
10+
from traitlets import default, Unicode
1111

12-
from .builder import DEFAULT_CPU_LIMIT, DEFAULT_MEMORY_LIMIT
13-
from .images import list_images, client
1412

15-
VOLUMES_PATH = "/home"
16-
SHARED_DATA_PATH = "/srv/data"
13+
class PlasmaBioSpawner(SystemUserSpawner, TLJHDockerSpawner):
1714

18-
# Default CPU period
19-
# See: https://docs.docker.com/config/containers/resource_constraints/#limit-a-containers-access-to-memory#configure-the-default-cfs-scheduler
20-
CPU_PERIOD = 100_000
15+
base_path = Unicode("/home", config=True, help="The base path for the user volumes")
2116

17+
shared_data_path = Unicode(
18+
"/srv/data", config=True, help="The path to the shared data folder"
19+
)
2220

23-
# See: https://github.com/jupyterhub/jupyterhub/tree/master/examples/bootstrap-script#example-1---create-a-user-directory
24-
def create_pre_spawn_hook(base_path):
25-
def pre_spawn_hook(spawner):
21+
def start(self):
22+
# set the image limits
23+
super().set_limits()
2624

27-
# create user directory on the host if it does not exist
28-
username = spawner.user.name
29-
imagename = spawner.user_options.get("image")
25+
# escape the image name
26+
username = self.user.name
27+
imagename = self.user_options.get("image")
3028
imagename_escaped = imagename.replace(":", "-").replace("/", "-")
3129

32-
volume_path = os.path.join(base_path, username, imagename_escaped)
30+
# create the user directory on the host if it does not exist
31+
volume_path = os.path.join(self.base_path, username, imagename_escaped)
3332
os.makedirs(volume_path, exist_ok=True)
3433

3534
# the escaped image name is used to create a new folder in the user home directory
36-
spawner.host_homedir_format_string = f"{base_path}/{username}"
35+
self.host_homedir_format_string = f"{self.base_path}/{username}"
3736
# pass the image name to the Docker container
38-
spawner.environment = {"USER_IMAGE": imagename_escaped}
39-
40-
# set the image limits
41-
image = client.images.get(imagename)
42-
mem_limit = image.labels.get("plasmabio.mem_limit", None)
43-
cpu_limit = image.labels.get("plasmabio.cpu_limit", None)
44-
spawner.mem_limit = mem_limit or spawner.mem_limit
45-
spawner.cpu_limit = float(cpu_limit) if cpu_limit else spawner.cpu_limit
46-
spawner.extra_host_config = {
47-
"cpu_period": CPU_PERIOD,
48-
"cpu_quota": int(float(CPU_PERIOD) * spawner.cpu_limit),
37+
self.environment = {"USER_IMAGE": imagename_escaped}
38+
39+
# mount volumes
40+
self.volumes = {
41+
os.path.join(
42+
os.path.dirname(__file__), "entrypoint", "entrypoint.sh"
43+
): "/usr/local/bin/repo2docker-entrypoint",
44+
self.shared_data_path: {"bind": "/srv/data", "mode": "ro"},
4945
}
5046

51-
return pre_spawn_hook
52-
53-
54-
def options_form(spawner):
55-
"""
56-
Override the default form to handle the case when there is only
57-
one image.
58-
"""
59-
images = spawner.image_whitelist(spawner)
60-
option_t = '<option value="{image}" {selected}>{image}</option>'
61-
options = [
62-
option_t.format(
63-
image=image, selected="selected" if image == spawner.image else ""
64-
)
65-
for image in images
66-
]
67-
return """
68-
<label for="image">Select an image:</label>
69-
<select class="form-control" name="image" required autofocus>
70-
{options}
71-
</select>
72-
""".format(
73-
options=options
74-
)
75-
76-
77-
def image_whitelist(spawner):
78-
"""
79-
Retrieve the list of available images
80-
"""
81-
images = list_images()
82-
return {image["image_name"]: image["image_name"] for image in images}
47+
return super().start()
8348

8449

85-
@hookimpl
50+
@hookimpl(trylast=True)
8651
def tljh_custom_jupyterhub_config(c):
8752
# hub
88-
c.JupyterHub.hub_ip = public_ips()[0]
8953
c.JupyterHub.cleanup_servers = False
9054
c.JupyterHub.authenticator_class = PAMAuthenticator
91-
c.JupyterHub.spawner_class = SystemUserSpawner
55+
c.JupyterHub.spawner_class = PlasmaBioSpawner
9256
c.JupyterHub.allow_named_servers = True
9357
c.JupyterHub.named_server_limit_per_user = 2
58+
c.JupyterHub.template_paths.insert(
59+
0, os.path.join(os.path.dirname(__file__), "templates")
60+
)
9461

9562
# spawner
63+
# update name template for named servers
64+
c.PlasmaBioSpawner.name_template = "{prefix}-{username}-{servername}"
9665
# increase the timeout to be able to pull larger Docker images
97-
c.SystemUserSpawner.start_timeout = 120
98-
c.SystemUserSpawner.pull_policy = "Never"
99-
c.SystemUserSpawner.name_template = "{prefix}-{username}-{servername}"
100-
c.SystemUserSpawner.default_url = "/lab"
66+
c.PlasmaBioSpawner.start_timeout = 120
67+
c.PlasmaBioSpawner.pull_policy = "Never"
68+
c.PlasmaBioSpawner.remove = True
69+
c.PlasmaBioSpawner.default_url = "/lab"
10170
# TODO: change back to jupyterhub-singleuser
102-
c.SystemUserSpawner.cmd = ["/srv/conda/envs/notebook/bin/jupyterhub-singleuser"]
103-
c.SystemUserSpawner.volumes = {
104-
os.path.join(
105-
os.path.dirname(__file__), "entrypoint", "entrypoint.sh"
106-
): "/usr/local/bin/repo2docker-entrypoint",
107-
SHARED_DATA_PATH: {"bind": "/srv/data", "mode": "ro"},
108-
}
109-
71+
c.PlasmaBioSpawner.cmd = ["/srv/conda/envs/notebook/bin/jupyterhub-singleuser"]
11072
# set the default cpu and memory limits
111-
c.SystemUserSpawner.mem_limit = DEFAULT_MEMORY_LIMIT
112-
c.SystemUserSpawner.cpu_limit = float(DEFAULT_CPU_LIMIT)
113-
c.SystemUserSpawner.args = ["--ResourceUseDisplay.track_cpu_percent=True"]
114-
115-
c.SystemUserSpawner.pre_spawn_hook = create_pre_spawn_hook(VOLUMES_PATH)
116-
c.SystemUserSpawner.remove = True
117-
c.SystemUserSpawner.image_whitelist = image_whitelist
118-
c.SystemUserSpawner.options_form = options_form
119-
120-
c.JupyterHub.template_paths = (
121-
os.path.join(os.path.dirname(__file__), "templates"),
122-
)
123-
124-
# register the service to manage the user images
125-
c.JupyterHub.services.append(
126-
{
127-
"name": "environments",
128-
"admin": True,
129-
"url": "http://127.0.0.1:9988",
130-
"command": [sys.executable, "-m", "tljh_plasmabio.images"],
131-
}
132-
)
73+
c.PlasmaBioSpawner.args = ["--ResourceUseDisplay.track_cpu_percent=True"]
13374

13475
# register Cockpit as a service if active
13576
if check_service_active("cockpit"):
13677
c.JupyterHub.services.append(
13778
{"name": "cockpit", "url": "http://0.0.0.0:9090",},
13879
)
139-
140-
141-
@hookimpl
142-
def tljh_extra_hub_pip_packages():
143-
return ["dockerspawner", "jupyter_client"]

0 commit comments

Comments
 (0)