Skip to content

Commit 190cc8f

Browse files
authored
Merge pull request #40 from shingo78/feature/repo2docker-service
cwh-repo2docker: Switch from c.JupyterHub.extra_handlers to services
2 parents ac4f3ae + 2736387 commit 190cc8f

14 files changed

Lines changed: 354 additions & 70 deletions

File tree

jupyterhub/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ RUN python3 -m pip install --no-cache /tmp/wheelhouse/*
6464
# Resources
6565
RUN mkdir /var/jupyterhub
6666
ADD jupyterhub_config.py /srv/jupyterhub/
67+
ADD cwh_repo2docker_config.py /srv/jupyterhub/
6768
ADD resources-schema.json /srv/jupyterhub/
6869
ADD get_user_id.sh /
6970
RUN chmod +x /get_user_id.sh
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
recursive-include cwh_repo2docker/custom_templates/ *
12
recursive-include cwh_repo2docker/templates/ *
23
recursive-include cwh_repo2docker/static/ *

jupyterhub/cwh-repo2docker/cwh_repo2docker/__init__.py

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import os
2+
import sys
23

34
from coursewareuserspawner import CoursewareUserSpawner
45
from jinja2 import Environment, BaseLoader
5-
from jupyterhub.handlers.static import CacheControlStaticFilesHandler
66
from traitlets import Unicode
7+
from tornado import web
78

8-
from .builder import BuildHandler, DefaultCouseImageHandler
99
from .registry import get_registry, split_image_name
10-
from .images import ImagesHandler
11-
from .logs import LogsHandler
1210

1311

1412
class Repo2DockerSpawner(CoursewareUserSpawner):
@@ -102,6 +100,19 @@ async def get_options_form(self):
102100
)
103101
return image_form_template.render(image_list=images, registry_host=self._registry.host)
104102

103+
async def check_allowed(self, image):
104+
images = await self._registry.list_images()
105+
106+
registry_host = self._registry.host
107+
image_names = [
108+
f'{registry_host}/{image["image_name"]}'
109+
for image in images
110+
]
111+
112+
if image not in image_names:
113+
raise web.HTTPError(400, "Specifying image to launch is not allowed")
114+
return image
115+
105116
def _use_default_course_image(self, images):
106117
self.image = self._registry.get_default_course_image()
107118

@@ -156,28 +167,49 @@ async def create_object(self, *args, **kwargs):
156167
return await super().create_object(*args, **kwargs)
157168

158169

159-
def cwh_repo2docker_jupyterhub_config(c):
170+
def cwh_repo2docker_jupyterhub_config(
171+
c,
172+
config_file=None,
173+
service_name='environments',
174+
custom_menu=False,
175+
service_environments={},
176+
debug=False):
160177
# hub
161178
c.JupyterHub.spawner_class = Repo2DockerSpawner
162179

163-
# add extra templates for the service UI
164-
c.JupyterHub.template_paths.insert(
165-
0, os.path.join(os.path.dirname(__file__), "templates")
166-
)
167-
168180
c.DockerSpawner.cmd = ["jupyterhub-singleuser"]
169181

170-
# register the handlers to manage the user images
171-
c.JupyterHub.extra_handlers.extend(
172-
[
173-
(r"environments", ImagesHandler),
174-
(r"api/environments", BuildHandler),
175-
(r"api/environments/default-course-image", DefaultCouseImageHandler),
176-
(r"api/environments/([^/]+)/logs", LogsHandler),
177-
(
178-
r"environments-static/(.*)",
179-
CacheControlStaticFilesHandler,
180-
{"path": os.path.join(os.path.dirname(__file__), "static")},
181-
),
182-
]
183-
)
182+
if custom_menu:
183+
# add extra templates for the service UI
184+
c.JupyterHub.template_paths.insert(
185+
0, os.path.join(os.path.dirname(__file__), "custom_templates")
186+
)
187+
188+
service_command = [
189+
sys.executable,
190+
"-m", "cwh_repo2docker.service",
191+
]
192+
193+
if config_file is not None:
194+
service_command.extend([
195+
"--config-file", config_file
196+
])
197+
198+
if debug:
199+
service_command.extend([
200+
"--debug"
201+
])
202+
203+
c.JupyterHub.template_vars.update({
204+
'cwh_repo2docker_service_name': service_name
205+
})
206+
207+
c.JupyterHub.services.extend([{
208+
"name": service_name,
209+
"command": service_command,
210+
"url": "http://127.0.0.1:10101",
211+
"display": not custom_menu,
212+
"oauth_no_confirm": True,
213+
"environment": service_environments,
214+
"oauth_client_allowed_scopes": ["inherit"]
215+
}])
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import json
2+
3+
from tornado import web
4+
from tornado.log import app_log
5+
6+
from jupyterhub.utils import url_path_join
7+
8+
9+
class BaseHandler(web.RequestHandler):
10+
@property
11+
def log(self):
12+
return self.settings.get('log', app_log)
13+
14+
def render_template(self, name, **ns):
15+
template_ns = {}
16+
template_ns.update(self.template_namespace)
17+
template_ns["xsrf_token"] = self.xsrf_token.decode("ascii")
18+
template_ns.update(ns)
19+
template = self.settings['jinja2_env'].get_template(name)
20+
return template.render_async(**template_ns)
21+
22+
@property
23+
def template_namespace(self):
24+
user = self.current_user
25+
ns = dict(
26+
base_url=url_path_join(
27+
self.settings['base_url'],
28+
self.settings['hub_prefix']),
29+
prefix=self.settings['base_url'],
30+
service_prefix=self.settings['service_prefix'],
31+
brand_text=self.settings['brand_text'],
32+
user=user,
33+
static_url=self.static_url,
34+
no_spawner_check=True
35+
)
36+
return ns
37+
38+
def get_json_body(self):
39+
"""Return the body of the request as JSON data."""
40+
if not self.request.body:
41+
return None
42+
body = self.request.body.strip().decode('utf-8')
43+
try:
44+
model = json.loads(body)
45+
except Exception:
46+
self.log.debug("Bad JSON: %r", body)
47+
self.log.error("Couldn't parse JSON", exc_info=True)
48+
raise web.HTTPError(400, 'Invalid JSON in body of request')
49+
return model

jupyterhub/cwh-repo2docker/cwh_repo2docker/builder.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,22 @@
22
import re
33

44
from aiodocker import Docker, DockerError
5-
from jupyterhub.apihandlers import APIHandler
6-
from jupyterhub.scopes import needs_scope
5+
from jupyterhub.services.auth import HubOAuthenticated
76
from tornado import web
87

98
from .docker import build_image
109
from .registry import get_registry, split_image_name
10+
from .base import BaseHandler
1111

1212
IMAGE_NAME_RE = r"^[a-z0-9-_]+$"
1313

1414

15-
class BuildHandler(APIHandler):
15+
class BuildHandler(HubOAuthenticated, BaseHandler):
1616
"""
1717
Handle requests to build user environments as Docker images
1818
"""
1919

2020
@web.authenticated
21-
@needs_scope('admin-ui')
2221
async def delete(self):
2322
data = self.get_json_body()
2423
name = data["name"]
@@ -37,10 +36,10 @@ async def delete(self):
3736
raise web.HTTPError(500, e.message)
3837

3938
self.set_status(200)
39+
self.set_header('content-type', 'application/json')
4040
self.finish(json.dumps({"status": "ok"}))
4141

4242
@web.authenticated
43-
@needs_scope('admin-ui')
4443
async def post(self):
4544
data = self.get_json_body()
4645
repo = data["repo"]
@@ -74,16 +73,16 @@ async def post(self):
7473
await build_image(registry.host, repo, ref, name, username, password, extra_buildargs)
7574

7675
self.set_status(200)
76+
self.set_header('content-type', 'application/json')
7777
self.finish(json.dumps({"status": "ok"}))
7878

7979

80-
class DefaultCouseImageHandler(APIHandler):
80+
class DefaultCourseImageHandler(HubOAuthenticated, BaseHandler):
8181
"""
8282
Handler to update the default course image
8383
"""
8484

8585
@web.authenticated
86-
@needs_scope('admin-ui')
8786
async def put(self):
8887
data = self.get_json_body()
8988
name = data["name"]
@@ -95,4 +94,5 @@ async def put(self):
9594
await registry.set_default_course_image(repo, digest)
9695

9796
self.set_status(200)
97+
self.set_header('content-type', 'application/json')
9898
self.finish(json.dumps({"status": "ok"}))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "templates/page.html" %}
2+
3+
{% block nav_bar_left_items %}
4+
{{ super() }}
5+
{% if 'access:services!service={{cwh_repo2docker_service_name}}' in expanded_scopes or 'access:services' in expanded_scopes %}
6+
<li><a href="{{prefix}}services/{{cwh_repo2docker_service_name}}/">Environments</a></li>
7+
{% endif %}
8+
{% endblock %}
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1-
from inspect import isawaitable
2-
from jupyterhub.handlers.base import BaseHandler
3-
from jupyterhub.scopes import needs_scope
1+
from jupyterhub.services.auth import HubOAuthenticated
42
from tornado import web
5-
import json
63

74
from .docker import list_containers
85
from .registry import get_registry
6+
from .base import BaseHandler
97

108

11-
class ImagesHandler(BaseHandler):
9+
class ImagesHandler(HubOAuthenticated, BaseHandler):
1210
"""
1311
Handler to show the list of environments as Docker images
1412
"""
1513

1614
@web.authenticated
17-
@needs_scope('admin-ui')
1815
async def get(self):
1916
registry = get_registry(config=self.settings['config'])
2017
images = await registry.list_images()
@@ -23,7 +20,4 @@ async def get(self):
2320
"images.html",
2421
images=images + containers
2522
)
26-
if isawaitable(result):
27-
self.write(await result)
28-
else:
29-
self.write(result)
23+
self.write(await result)

jupyterhub/cwh-repo2docker/cwh_repo2docker/logs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import json
22

33
from aiodocker import Docker
4-
from jupyterhub.apihandlers import APIHandler
5-
from jupyterhub.utils import admin_only
4+
from jupyterhub.services.auth import HubOAuthenticated
65
from tornado import web
76
from tornado.iostream import StreamClosedError
87

8+
from .base import BaseHandler
99

10-
class LogsHandler(APIHandler):
10+
11+
class LogsHandler(HubOAuthenticated, BaseHandler):
1112
"""
1213
Expose a handler to follow the build logs.
1314
"""
1415
@web.authenticated
15-
@admin_only
1616
async def get(self, name):
1717
self.set_header("Content-Type", "text/event-stream")
1818
self.set_header("Cache-Control", "no-cache")

0 commit comments

Comments
 (0)