diff --git a/jupyterhub_config.py b/jupyterhub_config.py index 7c430d7..dd838ea 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -45,13 +45,29 @@ sys.executable, "-m", "tljh_repo2docker", - "--TljhRepo2Docker.ip", + "--ip", "127.0.0.1", - "--TljhRepo2Docker.port", + "--port", "6789", ], "oauth_no_confirm": True, - "admin": True } ] -) \ No newline at end of file +) + +c.JupyterHub.load_roles = [ + { + "description": "Role for tljh_repo2docker service", + "name": "tljh_repo2docker_role", + "scopes": ["read:users", "read:servers", "read:roles:users"], + "services": ["tljh_repo2docker"], + }, + { + "name": "env-user", + "scopes": [ + # access to the env page + "access:services" + ], + "users": ["trung"], + }, +] diff --git a/src/environments/LogDialog.tsx b/src/environments/LogDialog.tsx index 2489354..45e95ca 100644 --- a/src/environments/LogDialog.tsx +++ b/src/environments/LogDialog.tsx @@ -42,10 +42,10 @@ function _EnvironmentLogButton(props: IEnvironmentLogButton) { terminal.open(divRef.current); fitAddon.fit(); - const { baseUrl, xsrfToken } = jhData; + const { servicePrefix, xsrfToken } = jhData; let logsUrl = urlJoin( - baseUrl, + servicePrefix, 'api', 'environments', props.image, diff --git a/src/servers/OpenServerButton.tsx b/src/servers/OpenServerButton.tsx index 256812a..5aa2a97 100644 --- a/src/servers/OpenServerButton.tsx +++ b/src/servers/OpenServerButton.tsx @@ -20,9 +20,9 @@ function _OpenServerButton(props: IOpenServerButton) { const [progress, setProgress] = useState(0); useEffect(() => { - const { user, baseUrl, xsrfToken } = jhData; + const { user, hubPrefix, xsrfToken } = jhData; let progressUrl = urlJoin( - baseUrl, + hubPrefix, 'api', 'users', user, diff --git a/tljh_repo2docker/__init__.py b/tljh_repo2docker/__init__.py index aceeabc..edcedb1 100644 --- a/tljh_repo2docker/__init__.py +++ b/tljh_repo2docker/__init__.py @@ -1,18 +1,14 @@ -import os - - from aiodocker import Docker from dockerspawner import DockerSpawner -from jinja2 import Environment, BaseLoader +from jinja2 import BaseLoader, Environment from jupyter_client.localinterfaces import public_ips - from jupyterhub.traitlets import ByteSpecification -from tljh.hooks import hookimpl from tljh.configurer import load_config +from tljh.hooks import hookimpl from traitlets import Unicode from traitlets.config import Configurable -from .docker import list_images +from .docker import list_images # Default CPU period # See: https://docs.docker.com/config/containers/resource_constraints/#limit-a-containers-access-to-memory#configure-the-default-cfs-scheduler diff --git a/tljh_repo2docker/app.py b/tljh_repo2docker/app.py index 3549acc..a07d551 100644 --- a/tljh_repo2docker/app.py +++ b/tljh_repo2docker/app.py @@ -3,20 +3,19 @@ import socket import typing as tp from pathlib import Path -from urllib.parse import urlsplit, urlunsplit -from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader +from jinja2 import Environment, PackageLoader +from jupyterhub.app import DATA_FILES_PATH +from jupyterhub.handlers.static import LogoHandler +from jupyterhub.utils import url_path_join from tornado import ioloop, web -from tornado.log import access_log, app_log, gen_log from traitlets import Dict, Int, List, Unicode, default, validate from traitlets.config.application import Application -from jupyterhub.utils import url_path_join -from jupyterhub.app import DATA_FILES_PATH -from jupyterhub.handlers.static import LogoHandler + from .builder import BuildHandler -from .servers import ServersHandler from .environments import EnvironmentsHandler from .logs import LogsHandler +from .servers import ServersHandler if os.environ.get("JUPYTERHUB_API_TOKEN"): from jupyterhub.services.auth import HubOAuthCallbackHandler @@ -33,8 +32,6 @@ def get(self): class TljhRepo2Docker(Application): name = Unicode("tljh-repo2docker") - version = "1.0.0" - port = Int(6789, help="Port of the service", config=True) base_url = Unicode(help="JupyterHub base URL", config=True) @@ -105,6 +102,8 @@ def _logo_file_default(self): def _default_log_level(self): return logging.INFO + aliases = {"port": "TljhRepo2Docker.port", "ip": "TljhRepo2Docker.ip"} + def init_settings(self) -> tp.Dict: """Initialize settings for the service application.""" static_path = DATA_FILES_PATH + "/static/" @@ -152,10 +151,15 @@ def init_handlers(self) -> tp.List: ), (self.service_prefix, web.RedirectHandler, {"url": server_url}), (server_url, ServersHandler), - (url_path_join(self.service_prefix, r"environments"), EnvironmentsHandler), + ( + url_path_join(self.service_prefix, r"environments"), + EnvironmentsHandler, + ), (url_path_join(self.service_prefix, r"api/environments"), BuildHandler), ( - url_path_join(self.service_prefix, r"api/environments/([^/]+)/logs"), + url_path_join( + self.service_prefix, r"api/environments/([^/]+)/logs" + ), LogsHandler, ), ] @@ -194,4 +198,4 @@ def start(self): self.log.info("Stopping...") -main = TljhRepo2Docker.launch_instance \ No newline at end of file +main = TljhRepo2Docker.launch_instance diff --git a/tljh_repo2docker/base.py b/tljh_repo2docker/base.py index 1ac6146..d835e15 100644 --- a/tljh_repo2docker/base.py +++ b/tljh_repo2docker/base.py @@ -1,30 +1,30 @@ +import json import os -from typing import Dict -from tornado import web -from jupyterhub.utils import url_path_join -from jupyterhub.services.auth import HubOAuthenticated + +from httpx import AsyncClient from jinja2 import Template +from jupyterhub.services.auth import HubOAuthenticated +from jupyterhub.utils import url_path_join +from tornado import web +from jupyterhub.scopes import needs_scope from .model import UserModel -from httpx import AsyncClient -import json - -JUPYTERHUB_API_URL = os.environ.get("JUPYTERHUB_API_URL", "") -API_TOKEN = os.environ.get("JUPYTERHUB_API_TOKEN", None) -print("JUPYTERHUB_API_URL", JUPYTERHUB_API_URL) class BaseHandler(HubOAuthenticated, web.RequestHandler): """ Base handler for tljh_repo2docker service """ + _client = None @property def client(self): if not BaseHandler._client: + api_url = os.environ.get("JUPYTERHUB_API_URL", "") + api_token = os.environ.get("JUPYTERHUB_API_TOKEN", None) BaseHandler._client = AsyncClient( - base_url=JUPYTERHUB_API_URL, - headers={f"Authorization": f"Bearer {API_TOKEN}"}, + base_url=api_url, + headers={f"Authorization": f"Bearer {api_token}"}, ) return BaseHandler._client @@ -32,7 +32,11 @@ async def fetch_user(self) -> UserModel: user = self.current_user url = url_path_join("users", user["name"]) response = await self.client.get(url + "?include_stopped_servers") - user_model = response.json() + user_model: dict = response.json() + user_model.setdefault("name", user["name"]) + user_model.setdefault("servers", {}) + user_model.setdefault("admin", False) + return UserModel.from_dict(user_model) def get_template(self, name: str) -> Template: diff --git a/tljh_repo2docker/builder.py b/tljh_repo2docker/builder.py index 440072d..ec7b1d9 100644 --- a/tljh_repo2docker/builder.py +++ b/tljh_repo2docker/builder.py @@ -2,10 +2,9 @@ import re from aiodocker import Docker, DockerError -from .base import BaseHandler -from jupyterhub.utils import admin_only from tornado import web +from .base import BaseHandler from .docker import build_image IMAGE_NAME_RE = r"^[a-z0-9-_]+$" @@ -73,4 +72,4 @@ async def post(self): ) self.set_status(200) - self.finish(json.dumps({"status": "ok"})) \ No newline at end of file + self.finish(json.dumps({"status": "ok"})) diff --git a/tljh_repo2docker/docker.py b/tljh_repo2docker/docker.py index 4f99220..f23a3ac 100644 --- a/tljh_repo2docker/docker.py +++ b/tljh_repo2docker/docker.py @@ -1,5 +1,4 @@ import json - from urllib.parse import urlparse from aiodocker import Docker @@ -143,4 +142,4 @@ async def build_image( ) async with Docker() as docker: - await docker.containers.run(config=config) \ No newline at end of file + await docker.containers.run(config=config) diff --git a/tljh_repo2docker/environments.py b/tljh_repo2docker/environments.py index 21e3ab2..1925c05 100644 --- a/tljh_repo2docker/environments.py +++ b/tljh_repo2docker/environments.py @@ -1,8 +1,7 @@ from inspect import isawaitable -from .base import BaseHandler -from jupyterhub.utils import admin_only -from tornado import web +from tornado import web +from .base import BaseHandler from .docker import list_containers, list_images @@ -14,11 +13,9 @@ class EnvironmentsHandler(BaseHandler): @web.authenticated async def get(self): user = self.current_user - if not user['admin']: - raise web.HTTPError( - status_code=404, - reason="Unauthorized." - ) + + if not user["admin"]: + raise web.HTTPError(status_code=404, reason="Unauthorized.") images = await list_images() containers = await list_containers() result = self.render_template( @@ -26,9 +23,9 @@ async def get(self): images=images + containers, default_mem_limit=self.settings.get("default_mem_limit"), default_cpu_limit=self.settings.get("default_cpu_limit"), - machine_profiles=self.settings.get("machine_profiles",[]), + machine_profiles=self.settings.get("machine_profiles", []), ) if isawaitable(result): self.write(await result) else: - self.write(result) \ No newline at end of file + self.write(result) diff --git a/tljh_repo2docker/logs.py b/tljh_repo2docker/logs.py index 7dbecdf..2a1b4b4 100644 --- a/tljh_repo2docker/logs.py +++ b/tljh_repo2docker/logs.py @@ -1,12 +1,12 @@ import json from aiodocker import Docker -from .base import BaseHandler -from jupyterhub.utils import admin_only from tornado import web from tornado.ioloop import IOLoop from tornado.iostream import StreamClosedError +from .base import BaseHandler + class LogsHandler(BaseHandler): """ diff --git a/tljh_repo2docker/model.py b/tljh_repo2docker/model.py index dfeb027..04de93d 100644 --- a/tljh_repo2docker/model.py +++ b/tljh_repo2docker/model.py @@ -1,5 +1,6 @@ from dataclasses import dataclass, fields + @dataclass class UserModel: @@ -28,4 +29,4 @@ def all_spawners(self) -> list: "user_options": server.get("user_options", None), } ) - return sp \ No newline at end of file + return sp diff --git a/tljh_repo2docker/servers.py b/tljh_repo2docker/servers.py index d3f4054..192ce24 100644 --- a/tljh_repo2docker/servers.py +++ b/tljh_repo2docker/servers.py @@ -1,7 +1,8 @@ from inspect import isawaitable -from .base import BaseHandler + from tornado import web +from .base import BaseHandler from .docker import list_images diff --git a/tljh_repo2docker/tests/conftest.py b/tljh_repo2docker/tests/conftest.py index 5b7ee87..b548404 100644 --- a/tljh_repo2docker/tests/conftest.py +++ b/tljh_repo2docker/tests/conftest.py @@ -1,9 +1,9 @@ import pytest - from aiodocker import Docker, DockerError -from tljh_repo2docker import tljh_custom_jupyterhub_config from traitlets.config import Config +from tljh_repo2docker import tljh_custom_jupyterhub_config + async def remove_docker_image(image_name): async with Docker() as docker: diff --git a/tljh_repo2docker/tests/test_builder.py b/tljh_repo2docker/tests/test_builder.py index f175834..e9257b9 100644 --- a/tljh_repo2docker/tests/test_builder.py +++ b/tljh_repo2docker/tests/test_builder.py @@ -1,8 +1,7 @@ import pytest - from aiodocker import Docker, DockerError -from .utils import add_environment, wait_for_image, remove_environment +from .utils import add_environment, remove_environment, wait_for_image @pytest.mark.asyncio diff --git a/tljh_repo2docker/tests/test_images.py b/tljh_repo2docker/tests/test_images.py index ddf38d8..b2cf1d9 100644 --- a/tljh_repo2docker/tests/test_images.py +++ b/tljh_repo2docker/tests/test_images.py @@ -1,5 +1,4 @@ import pytest - from jupyterhub.tests.utils import get_page from .utils import add_environment, wait_for_image diff --git a/tljh_repo2docker/tests/test_logs.py b/tljh_repo2docker/tests/test_logs.py index e41a784..09eb496 100644 --- a/tljh_repo2docker/tests/test_logs.py +++ b/tljh_repo2docker/tests/test_logs.py @@ -1,7 +1,6 @@ import json import pytest - from jupyterhub.tests.utils import api_request, async_requests from .utils import add_environment, wait_for_image