From 181bd36acd16eb236d0815c200bfdea6bc07f9c8 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Fri, 29 Mar 2024 17:43:05 +0100 Subject: [PATCH 1/6] Start server from backend --- jupyterhub_config.py | 7 +++---- src/common/axiosclient.ts | 6 +++--- src/servers/NewServerDialog.tsx | 22 +++++++++------------- src/servers/types.ts | 1 + tljh_repo2docker/app.py | 3 ++- tljh_repo2docker/base.py | 2 +- tljh_repo2docker/servers.py | 27 ++++++++++++++++++++++++++- 7 files changed, 45 insertions(+), 23 deletions(-) diff --git a/jupyterhub_config.py b/jupyterhub_config.py index ec27fd9..e57654a 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -37,8 +37,7 @@ "--machine_profiles", '{"label": "Medium", "cpu": 4, "memory": 4}', "--machine_profiles", - '{"label": "Large", "cpu": 8, "memory": 8}' - + '{"label": "Large", "cpu": 8, "memory": 8}', ], "oauth_no_confirm": True, "oauth_client_allowed_scopes": [ @@ -58,11 +57,11 @@ { "description": "Role for tljh_repo2docker service", "name": "tljh-repo2docker-service", - "scopes": ["read:users", "read:servers", "read:roles:users"], + "scopes": ["read:users", "read:servers", "read:roles:users", "admin:servers"], "services": ["tljh_repo2docker"], }, { - "name": 'tljh-repo2docker-service-admin', + "name": "tljh-repo2docker-service-admin", "users": ["alice"], "scopes": [TLJH_R2D_ADMIN_SCOPE], }, diff --git a/src/common/axiosclient.ts b/src/common/axiosclient.ts index 3be8759..79b8387 100644 --- a/src/common/axiosclient.ts +++ b/src/common/axiosclient.ts @@ -15,15 +15,15 @@ export class AxiosClient { async request(args: { method: 'get' | 'post' | 'put' | 'option' | 'delete'; - prefix: 'api' | 'spawn'; + prefix?: 'api' | 'spawn'; path: string; query?: string; data?: { [key: string]: any } | FormData; }): Promise { const { method, path } = args; - + const prefix = args.prefix ?? ''; const data = args.data ?? {}; - let url = urlJoin(args.prefix, encodeUriComponents(path)); + let url = urlJoin(prefix, encodeUriComponents(path)); if (args.query) { const sep = url.indexOf('?') === -1 ? '?' : '&'; url = `${url}${sep}${args.query}`; diff --git a/src/servers/NewServerDialog.tsx b/src/servers/NewServerDialog.tsx index 09f0309..d359f33 100644 --- a/src/servers/NewServerDialog.tsx +++ b/src/servers/NewServerDialog.tsx @@ -16,8 +16,8 @@ import { IEnvironmentData } from '../environments/types'; import { SmallTextField } from '../common/SmallTextField'; import { useAxios } from '../common/AxiosContext'; -import { SPAWN_PREFIX } from '../common/axiosclient'; import { useJupyterhub } from '../common/JupyterhubContext'; +import { SERVER_PREFIX } from './types'; export interface INewServerDialogProps { images: IEnvironmentData[]; allowNamedServers: boolean; @@ -65,22 +65,18 @@ function _NewServerDialog(props: INewServerDialogProps) { const createServer = useCallback(async () => { const imageName = props.images[rowSelectionModel[0] as number].image_name; - const data = new FormData(); - data.append('image', imageName); - let path = ''; - if (serverName.length > 0) { - path = `${jhData.user}/${serverName}`; - } else { - path = jhData.user; - } + const data: { [key: string]: string } = { + imageName, + userName: jhData.user, + serverName + }; try { - await axios.hubClient.request({ + await axios.serviceClient.request({ method: 'post', - prefix: SPAWN_PREFIX, - path, + path: SERVER_PREFIX, data }); - window.location.reload(); + // window.location.reload(); } catch (e: any) { console.error(e); } diff --git a/src/servers/types.ts b/src/servers/types.ts index a1da1d9..cf51864 100644 --- a/src/servers/types.ts +++ b/src/servers/types.ts @@ -1,3 +1,4 @@ +export const SERVER_PREFIX = 'servers'; export interface IServerData { name: string; url: string; diff --git a/tljh_repo2docker/app.py b/tljh_repo2docker/app.py index 9254cbc..3aa762e 100644 --- a/tljh_repo2docker/app.py +++ b/tljh_repo2docker/app.py @@ -157,6 +157,7 @@ def init_handlers(self) -> tp.List: handlers = [] static_path = str(HERE / "static") server_url = url_path_join(self.service_prefix, r"servers") + environments_url = url_path_join(self.service_prefix, r"environments") handlers.extend( [ ( @@ -176,7 +177,7 @@ 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"), + environments_url, EnvironmentsHandler, ), (url_path_join(self.service_prefix, r"api/environments"), BuildHandler), diff --git a/tljh_repo2docker/base.py b/tljh_repo2docker/base.py index 98fb7da..4d80fe7 100644 --- a/tljh_repo2docker/base.py +++ b/tljh_repo2docker/base.py @@ -42,7 +42,7 @@ def client(self): api_token = os.environ.get("JUPYTERHUB_API_TOKEN", None) BaseHandler._client = AsyncClient( base_url=api_url, - headers={f"Authorization": f"Bearer {api_token}"}, + headers={"Authorization": f"Bearer {api_token}"}, ) return BaseHandler._client diff --git a/tljh_repo2docker/servers.py b/tljh_repo2docker/servers.py index 8a84c83..7dea47e 100644 --- a/tljh_repo2docker/servers.py +++ b/tljh_repo2docker/servers.py @@ -1,7 +1,8 @@ from inspect import isawaitable +import requests from tornado import web - +from jupyterhub.utils import url_path_join from .base import BaseHandler from .docker import list_images @@ -33,3 +34,27 @@ async def get(self): self.write(await result) else: self.write(result) + + @web.authenticated + async def post(self): + data = self.get_json_body() + image_name = data.get("imageName", None) + user_name = data.get("userName", None) + server_name = data.get("serverName", "") + if user_name != self.current_user["name"]: + raise web.HTTPError(403, "Unauthorized") + if not image_name: + raise web.HTTPError(400, "Missing image name") + + post_data = {"image": image_name} + + path = "" + if len(server_name) > 0: + path = url_path_join("users", user_name, "servers", server_name) + else: + path = url_path_join("users", user_name, "server") + try: + response = await self.client.post(path, json=post_data) + response.raise_for_status() + except requests.exceptions.HTTPError as e: + print(e) From a901b46893124fb1af31422e4b2a7db8da78dfd1 Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Tue, 2 Apr 2024 16:59:11 +0200 Subject: [PATCH 2/6] Start and delete server via service --- jupyterhub_config.py | 2 ++ src/servers/NewServerDialog.tsx | 4 ++-- src/servers/OpenServerButton.tsx | 16 +++++++++------- src/servers/RemoveServerButton.tsx | 20 ++++++++------------ tljh_repo2docker/base.py | 1 + tljh_repo2docker/servers.py | 28 +++++++++++++++++++++++++--- 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/jupyterhub_config.py b/jupyterhub_config.py index e57654a..975b0c1 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -14,6 +14,8 @@ tljh_custom_jupyterhub_config(c) +c.JupyterHub.default_url = '/services/tljh_repo2docker/' + c.JupyterHub.authenticator_class = DummyAuthenticator c.JupyterHub.allow_named_servers = True diff --git a/src/servers/NewServerDialog.tsx b/src/servers/NewServerDialog.tsx index d359f33..0baaae7 100644 --- a/src/servers/NewServerDialog.tsx +++ b/src/servers/NewServerDialog.tsx @@ -76,9 +76,9 @@ function _NewServerDialog(props: INewServerDialogProps) { path: SERVER_PREFIX, data }); - // window.location.reload(); + window.location.reload(); } catch (e: any) { - console.error(e); + alert(e); } }, [serverName, rowSelectionModel, props.images, axios, jhData]); const disabled = useMemo(() => { diff --git a/src/servers/OpenServerButton.tsx b/src/servers/OpenServerButton.tsx index 5aa2a97..873f06e 100644 --- a/src/servers/OpenServerButton.tsx +++ b/src/servers/OpenServerButton.tsx @@ -5,7 +5,7 @@ import { useAxios } from '../common/AxiosContext'; import { useJupyterhub } from '../common/JupyterhubContext'; import urlJoin from 'url-join'; import SyncIcon from '@mui/icons-material/Sync'; -import { SPAWN_PREFIX } from '../common/axiosclient'; +import { SERVER_PREFIX } from './types'; interface IOpenServerButton { url: string; @@ -30,7 +30,7 @@ function _OpenServerButton(props: IOpenServerButton) { props.serverName, 'progress' ); - if (xsrfToken) { + if (!xsrfToken) { // add xsrf token to url parameter const sep = progressUrl.indexOf('?') === -1 ? '?' : '&'; progressUrl = progressUrl + sep + '_xsrf=' + xsrfToken; @@ -51,13 +51,15 @@ function _OpenServerButton(props: IOpenServerButton) { const createServer = useCallback(async () => { const imageName = props.imageName; - const data = new FormData(); - data.append('image', imageName); + const data = { + imageName, + userName: jhData.user, + serverName: props.serverName + }; try { - await axios.hubClient.request({ + await axios.serviceClient.request({ method: 'post', - prefix: SPAWN_PREFIX, - path: `${jhData.user}/${props.serverName}`, + path: SERVER_PREFIX, data }); window.open(props.url, '_blank')?.focus(); diff --git a/src/servers/RemoveServerButton.tsx b/src/servers/RemoveServerButton.tsx index ebc2250..41ff42e 100644 --- a/src/servers/RemoveServerButton.tsx +++ b/src/servers/RemoveServerButton.tsx @@ -5,7 +5,7 @@ import { memo, useCallback } from 'react'; import { useAxios } from '../common/AxiosContext'; import { ButtonWithConfirm } from '../common/ButtonWithConfirm'; import { useJupyterhub } from '../common/JupyterhubContext'; -import { API_PREFIX } from '../common/axiosclient'; +import { SERVER_PREFIX } from './types'; interface IRemoveServerButton { server: string; @@ -15,24 +15,20 @@ function _RemoveServerButton(props: IRemoveServerButton) { const axios = useAxios(); const jhData = useJupyterhub(); const removeEnv = useCallback(async () => { - let path = ''; - if (props.server.length > 0) { - path = `users/${jhData.user}/servers/${props.server}`; - } else { - path = `users/${jhData.user}/server`; - } try { - await axios.hubClient.request({ + await axios.serviceClient.request({ method: 'delete', - prefix: API_PREFIX, - path, - data: { remove: props.server.length > 0 } + path: SERVER_PREFIX, + data: { + userName: jhData.user, + serverName: props.server + } }); window.location.reload(); } catch (e: any) { console.error(e); } - }, [props.server, axios, jhData]); + }, [props.server, axios, jhData.user]); return ( str: service_prefix=self.settings.get("service_prefix", "/"), hub_prefix=self.settings.get("hub_prefix", "/"), base_url=base_url, + logo_url=url_path_join(base_url,'hub','home'), logout_url=self.settings.get( "logout_url", url_path_join(base_url, "logout") ), diff --git a/tljh_repo2docker/servers.py b/tljh_repo2docker/servers.py index 7dea47e..e117389 100644 --- a/tljh_repo2docker/servers.py +++ b/tljh_repo2docker/servers.py @@ -1,6 +1,5 @@ from inspect import isawaitable -import requests from tornado import web from jupyterhub.utils import url_path_join from .base import BaseHandler @@ -56,5 +55,28 @@ async def post(self): try: response = await self.client.post(path, json=post_data) response.raise_for_status() - except requests.exceptions.HTTPError as e: - print(e) + except Exception: + raise web.HTTPError(500, "Server error") + self.finish(0) + + @web.authenticated + async def delete(self): + data = self.get_json_body() + user_name = data.get("userName", None) + server_name = data.get("serverName", "") + if user_name != self.current_user["name"]: + raise web.HTTPError(403, "Unauthorized") + + path = "" + post_data = {} + if len(server_name) > 0: + path = url_path_join("users", user_name, "servers", server_name) + post_data = {"remove": True} + else: + path = url_path_join("users", user_name, "server") + try: + response = await self.client.request("DELETE", path, json=post_data) + response.raise_for_status() + except Exception as e: + raise web.HTTPError(500, "Server error") + self.finish("0") From 74a4e4427f695eac072978c27d60ff61db68db60 Mon Sep 17 00:00:00 2001 From: trungleduc Date: Wed, 3 Apr 2024 00:49:57 +0200 Subject: [PATCH 3/6] Split server api handler --- src/common/ButtonWithConfirm.tsx | 9 +--- src/common/LoadingAnimation.tsx | 7 +++ src/common/axiosclient.ts | 6 ++- src/environments/EnvironmentList.tsx | 2 + src/servers/NewServerDialog.tsx | 8 +++ src/servers/OpenServerButton.tsx | 74 ++++------------------------ src/servers/RemoveServerButton.tsx | 1 + src/servers/ServersList.tsx | 1 - tljh_repo2docker/app.py | 8 ++- tljh_repo2docker/base.py | 2 +- tljh_repo2docker/model.py | 7 ++- tljh_repo2docker/servers.py | 50 +------------------ tljh_repo2docker/servers_api.py | 55 +++++++++++++++++++++ 13 files changed, 99 insertions(+), 131 deletions(-) create mode 100644 src/common/LoadingAnimation.tsx create mode 100644 tljh_repo2docker/servers_api.py diff --git a/src/common/ButtonWithConfirm.tsx b/src/common/ButtonWithConfirm.tsx index 97557c8..df7946a 100644 --- a/src/common/ButtonWithConfirm.tsx +++ b/src/common/ButtonWithConfirm.tsx @@ -3,9 +3,8 @@ import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import CircularProgress from '@mui/material/CircularProgress'; -import Box from '@mui/material/Box'; import { Fragment, memo, useCallback, useState } from 'react'; +import { Loading } from './LoadingAnimation'; interface IButtonWithConfirm { buttonLabel: string; @@ -15,11 +14,7 @@ interface IButtonWithConfirm { okLabel?: string; cancelLabel?: string; } -const Loading = () => ( - - - -); + function _ButtonWithConfirm(props: IButtonWithConfirm) { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); diff --git a/src/common/LoadingAnimation.tsx b/src/common/LoadingAnimation.tsx new file mode 100644 index 0000000..9d35aae --- /dev/null +++ b/src/common/LoadingAnimation.tsx @@ -0,0 +1,7 @@ +import CircularProgress from '@mui/material/CircularProgress'; +import Box from '@mui/material/Box'; +export const Loading = () => ( + + + +); diff --git a/src/common/axiosclient.ts b/src/common/axiosclient.ts index 79b8387..e22dfe9 100644 --- a/src/common/axiosclient.ts +++ b/src/common/axiosclient.ts @@ -19,8 +19,9 @@ export class AxiosClient { path: string; query?: string; data?: { [key: string]: any } | FormData; + params?: { [key: string]: string }; }): Promise { - const { method, path } = args; + const { method, path, params } = args; const prefix = args.prefix ?? ''; const data = args.data ?? {}; let url = urlJoin(prefix, encodeUriComponents(path)); @@ -35,7 +36,8 @@ export class AxiosClient { const response = await this._axios.request({ method, url, - data + data, + params }); return response.data; } diff --git a/src/environments/EnvironmentList.tsx b/src/environments/EnvironmentList.tsx index 7d2251f..25aff38 100644 --- a/src/environments/EnvironmentList.tsx +++ b/src/environments/EnvironmentList.tsx @@ -91,6 +91,7 @@ export interface IEnvironmentListProps { selectable?: boolean; rowSelectionModel?: GridRowSelectionModel; setRowSelectionModel?: (selected: GridRowSelectionModel) => void; + loading?: boolean; } function _EnvironmentList(props: IEnvironmentListProps) { @@ -111,6 +112,7 @@ function _EnvironmentList(props: IEnvironmentListProps) { return ( (''); const handleOpen = () => { setOpen(true); @@ -71,13 +73,16 @@ function _NewServerDialog(props: INewServerDialogProps) { serverName }; try { + setLoading(true); await axios.serviceClient.request({ method: 'post', + prefix: 'api', path: SERVER_PREFIX, data }); window.location.reload(); } catch (e: any) { + setLoading(false); alert(e); } }, [serverName, rowSelectionModel, props.images, axios, jhData]); @@ -102,6 +107,7 @@ function _NewServerDialog(props: INewServerDialogProps) { Server Options + {props.allowNamedServers && ( @@ -127,8 +133,10 @@ function _NewServerDialog(props: INewServerDialogProps) { selectable rowSelectionModel={rowSelectionModel} setRowSelectionModel={updateSelectedRow} + loading={loading} /> + - )} - - {!props.active && ( - - )} - - )} - {progress < 100 && ( - - - + {props.active && ( + )} + + {!props.active && } ); } diff --git a/src/servers/RemoveServerButton.tsx b/src/servers/RemoveServerButton.tsx index 41ff42e..34a90e1 100644 --- a/src/servers/RemoveServerButton.tsx +++ b/src/servers/RemoveServerButton.tsx @@ -19,6 +19,7 @@ function _RemoveServerButton(props: IRemoveServerButton) { await axios.serviceClient.request({ method: 'delete', path: SERVER_PREFIX, + prefix: 'api', data: { userName: jhData.user, serverName: props.server diff --git a/src/servers/ServersList.tsx b/src/servers/ServersList.tsx index 733ce86..086e605 100644 --- a/src/servers/ServersList.tsx +++ b/src/servers/ServersList.tsx @@ -93,7 +93,6 @@ function _ServerList(props: IServerListProps) { return allServers; }, [props]); - return ( tp.List: handlers = [] static_path = str(HERE / "static") server_url = url_path_join(self.service_prefix, r"servers") - environments_url = url_path_join(self.service_prefix, r"environments") handlers.extend( [ ( @@ -177,7 +177,11 @@ def init_handlers(self) -> tp.List: (self.service_prefix, web.RedirectHandler, {"url": server_url}), (server_url, ServersHandler), ( - environments_url, + url_path_join(self.service_prefix, r"api/servers"), + ServersAPIHandler, + ), + ( + url_path_join(self.service_prefix, r"environments"), EnvironmentsHandler, ), (url_path_join(self.service_prefix, r"api/environments"), BuildHandler), diff --git a/tljh_repo2docker/base.py b/tljh_repo2docker/base.py index 873f096..060b721 100644 --- a/tljh_repo2docker/base.py +++ b/tljh_repo2docker/base.py @@ -85,7 +85,7 @@ async def render_template(self, name: str, **kwargs) -> str: service_prefix=self.settings.get("service_prefix", "/"), hub_prefix=self.settings.get("hub_prefix", "/"), base_url=base_url, - logo_url=url_path_join(base_url,'hub','home'), + logo_url=url_path_join(base_url, "hub", "home"), logout_url=self.settings.get( "logout_url", url_path_join(base_url, "logout") ), diff --git a/tljh_repo2docker/model.py b/tljh_repo2docker/model.py index ec4f5d6..603e5c1 100644 --- a/tljh_repo2docker/model.py +++ b/tljh_repo2docker/model.py @@ -18,15 +18,14 @@ def from_dict(self, kwargs_dict: dict): def all_spawners(self) -> list: sp = [] for server in self.servers.values(): - if len(server["name"]) > 0: + active = bool(server.get("pending", None) or server.get("ready", False)) + if active or len(server["name"]) > 0: sp.append( { "name": server.get("name", ""), "url": server.get("url", ""), "last_activity": server.get("last_activity", None), - "active": bool( - server.get("pending", None) or server.get("ready", False) - ), + "active": active, "user_options": server.get("user_options", None), } ) diff --git a/tljh_repo2docker/servers.py b/tljh_repo2docker/servers.py index e117389..ea4abb2 100644 --- a/tljh_repo2docker/servers.py +++ b/tljh_repo2docker/servers.py @@ -1,7 +1,7 @@ from inspect import isawaitable from tornado import web -from jupyterhub.utils import url_path_join + from .base import BaseHandler from .docker import list_images @@ -17,7 +17,6 @@ async def get(self): user_data = await self.fetch_user() server_data = user_data.all_spawners() - named_server_limit = 0 result = self.render_template( "servers.html", @@ -33,50 +32,3 @@ async def get(self): self.write(await result) else: self.write(result) - - @web.authenticated - async def post(self): - data = self.get_json_body() - image_name = data.get("imageName", None) - user_name = data.get("userName", None) - server_name = data.get("serverName", "") - if user_name != self.current_user["name"]: - raise web.HTTPError(403, "Unauthorized") - if not image_name: - raise web.HTTPError(400, "Missing image name") - - post_data = {"image": image_name} - - path = "" - if len(server_name) > 0: - path = url_path_join("users", user_name, "servers", server_name) - else: - path = url_path_join("users", user_name, "server") - try: - response = await self.client.post(path, json=post_data) - response.raise_for_status() - except Exception: - raise web.HTTPError(500, "Server error") - self.finish(0) - - @web.authenticated - async def delete(self): - data = self.get_json_body() - user_name = data.get("userName", None) - server_name = data.get("serverName", "") - if user_name != self.current_user["name"]: - raise web.HTTPError(403, "Unauthorized") - - path = "" - post_data = {} - if len(server_name) > 0: - path = url_path_join("users", user_name, "servers", server_name) - post_data = {"remove": True} - else: - path = url_path_join("users", user_name, "server") - try: - response = await self.client.request("DELETE", path, json=post_data) - response.raise_for_status() - except Exception as e: - raise web.HTTPError(500, "Server error") - self.finish("0") diff --git a/tljh_repo2docker/servers_api.py b/tljh_repo2docker/servers_api.py new file mode 100644 index 0000000..7d30693 --- /dev/null +++ b/tljh_repo2docker/servers_api.py @@ -0,0 +1,55 @@ +from jupyterhub.utils import url_path_join +from tornado import web + +from .base import BaseHandler + + +class ServersAPIHandler(BaseHandler): + """ + Handler to manage single servers + """ + + @web.authenticated + async def post(self): + data = self.get_json_body() + image_name = data.get("imageName", None) + user_name = data.get("userName", None) + server_name = data.get("serverName", "") + if user_name != self.current_user["name"]: + raise web.HTTPError(403, "Unauthorized") + if not image_name: + raise web.HTTPError(400, "Missing image name") + + post_data = {"image": image_name} + + path = "" + if len(server_name) > 0: + path = url_path_join("users", user_name, "servers", server_name) + else: + path = url_path_join("users", user_name, "server") + try: + response = await self.client.post(path, json=post_data) + response.raise_for_status() + except Exception: + raise web.HTTPError(500, "Server error") + + @web.authenticated + async def delete(self): + data = self.get_json_body() + user_name = data.get("userName", None) + server_name = data.get("serverName", "") + if user_name != self.current_user["name"]: + raise web.HTTPError(403, "Unauthorized") + + path = "" + post_data = {} + if len(server_name) > 0: + path = url_path_join("users", user_name, "servers", server_name) + post_data = {"remove": True} + else: + path = url_path_join("users", user_name, "server") + try: + response = await self.client.request("DELETE", path, json=post_data) + response.raise_for_status() + except Exception: + raise web.HTTPError(500, "Server error") From e716fd5132db1e2a2c5e603276971b84d51cc8b5 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Wed, 3 Apr 2024 10:47:01 +0200 Subject: [PATCH 4/6] Handle missing `ContainerConfig` key --- jupyterhub_config.py | 1 - tljh_repo2docker/__init__.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/jupyterhub_config.py b/jupyterhub_config.py index 975b0c1..f40679a 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -14,7 +14,6 @@ tljh_custom_jupyterhub_config(c) -c.JupyterHub.default_url = '/services/tljh_repo2docker/' c.JupyterHub.authenticator_class = DummyAuthenticator diff --git a/tljh_repo2docker/__init__.py b/tljh_repo2docker/__init__.py index cbf6269..8d96e4d 100644 --- a/tljh_repo2docker/__init__.py +++ b/tljh_repo2docker/__init__.py @@ -128,12 +128,12 @@ async def set_limits(self): imagename = self.user_options.get("image") async with Docker() as docker: image = await docker.images.inspect(imagename) - mem_limit = image["ContainerConfig"]["Labels"].get( - "tljh_repo2docker.mem_limit", None - ) - cpu_limit = image["ContainerConfig"]["Labels"].get( - "tljh_repo2docker.cpu_limit", None - ) + config = image.get("ContainerConfig", None) + if not config: + config = image.get("Config", {}) + label = config.get("Labels", {}) + mem_limit = label.get("tljh_repo2docker.mem_limit", None) + cpu_limit = label.get("tljh_repo2docker.cpu_limit", None) # override the spawner limits if defined in the image if mem_limit: From 8361cb9e174d9ae6d8653b5c93113dc021dba94e Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Wed, 3 Apr 2024 11:41:18 +0200 Subject: [PATCH 5/6] Remove unused hub client --- src/common/AxiosContext.tsx | 3 +-- src/common/axiosclient.ts | 5 +---- src/environments/App.tsx | 8 +------- src/environments/NewEnvironmentDialog.tsx | 2 -- src/environments/RemoveEnvironmentButton.tsx | 2 -- src/servers/App.tsx | 8 +------- src/servers/NewServerDialog.tsx | 1 - src/servers/OpenServerButton.tsx | 1 - src/servers/RemoveServerButton.tsx | 1 - 9 files changed, 4 insertions(+), 27 deletions(-) diff --git a/src/common/AxiosContext.tsx b/src/common/AxiosContext.tsx index 7fffa60..15fdd81 100644 --- a/src/common/AxiosContext.tsx +++ b/src/common/AxiosContext.tsx @@ -2,9 +2,8 @@ import { createContext, useContext } from 'react'; import { AxiosClient } from './axiosclient'; export const AxiosContext = createContext<{ - hubClient: AxiosClient; serviceClient: AxiosClient; -}>({ hubClient: new AxiosClient({}), serviceClient: new AxiosClient({}) }); +}>({ serviceClient: new AxiosClient({}) }); export const useAxios = () => { return useContext(AxiosContext); diff --git a/src/common/axiosclient.ts b/src/common/axiosclient.ts index e22dfe9..2d3c60e 100644 --- a/src/common/axiosclient.ts +++ b/src/common/axiosclient.ts @@ -2,8 +2,6 @@ import urlJoin from 'url-join'; import { encodeUriComponents } from './utils'; import axios, { AxiosInstance } from 'axios'; -export const API_PREFIX = 'api'; -export const SPAWN_PREFIX = 'spawn'; export class AxiosClient { constructor(options: AxiosClient.IOptions) { this._baseUrl = options.baseUrl ?? ''; @@ -15,14 +13,13 @@ export class AxiosClient { async request(args: { method: 'get' | 'post' | 'put' | 'option' | 'delete'; - prefix?: 'api' | 'spawn'; path: string; query?: string; data?: { [key: string]: any } | FormData; params?: { [key: string]: string }; }): Promise { const { method, path, params } = args; - const prefix = args.prefix ?? ''; + const prefix = 'api'; const data = args.data ?? {}; let url = urlJoin(prefix, encodeUriComponents(path)); if (args.query) { diff --git a/src/environments/App.tsx b/src/environments/App.tsx index 587cfd3..6a3779c 100644 --- a/src/environments/App.tsx +++ b/src/environments/App.tsx @@ -20,12 +20,6 @@ export interface IAppProps { export default function App(props: IAppProps) { const jhData = useJupyterhub(); - const hubClient = useMemo(() => { - const baseUrl = jhData.hubPrefix; - const xsrfToken = jhData.xsrfToken; - return new AxiosClient({ baseUrl, xsrfToken }); - }, [jhData]); - const serviceClient = useMemo(() => { const baseUrl = jhData.servicePrefix; const xsrfToken = jhData.xsrfToken; @@ -34,7 +28,7 @@ export default function App(props: IAppProps) { return ( - + { const response = await axios.serviceClient.request({ method: 'delete', - prefix: API_PREFIX, path: ENV_PREFIX, data: { name: props.image } }); diff --git a/src/servers/App.tsx b/src/servers/App.tsx index 4eff3c2..d5b4f51 100644 --- a/src/servers/App.tsx +++ b/src/servers/App.tsx @@ -22,12 +22,6 @@ export interface IAppProps { export default function App(props: IAppProps) { const jhData = useJupyterhub(); - const hubClient = useMemo(() => { - const baseUrl = jhData.hubPrefix; - const xsrfToken = jhData.xsrfToken; - return new AxiosClient({ baseUrl, xsrfToken }); - }, [jhData]); - const serviceClient = useMemo(() => { const baseUrl = jhData.servicePrefix; const xsrfToken = jhData.xsrfToken; @@ -36,7 +30,7 @@ export default function App(props: IAppProps) { return ( - + Date: Wed, 3 Apr 2024 11:52:48 +0200 Subject: [PATCH 6/6] Update README --- README.md | 6 ++++-- jupyterhub_config.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 97a826b..2df4ef6 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,13 @@ The available settings for this service are: - `default_cpu_limit`: Default CPU limit of a user server; defaults to `None` - `machine_profiles`: Instead of entering directly the CPU and Memory value, `tljh-repo2docker` can be configured with pre-defined machine profiles and users can only choose from the available option; defaults to `[]` -Here is an example of registering `tljh_repo2docker`'s service with JupyterHub +This service requires the following scopes : `read:users`, `admin:servers` and `read:roles:users`. Here is an example of registering `tljh_repo2docker`'s service with JupyterHub ```python # jupyterhub_config.py from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE +import sys c.JupyterHub.services.extend( [ @@ -77,7 +78,7 @@ c.JupyterHub.load_roles = [ { "description": "Role for tljh_repo2docker service", "name": "tljh-repo2docker-service", - "scopes": ["read:users", "read:servers", "read:roles:users"], + "scopes": ["read:users", "admin:servers", "read:roles:users"], "services": ["tljh_repo2docker"], }, { @@ -99,6 +100,7 @@ Here is an example of the configuration # jupyterhub_config.py from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE +import sys c.JupyterHub.services.extend( [ diff --git a/jupyterhub_config.py b/jupyterhub_config.py index f40679a..05be037 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -58,7 +58,7 @@ { "description": "Role for tljh_repo2docker service", "name": "tljh-repo2docker-service", - "scopes": ["read:users", "read:servers", "read:roles:users", "admin:servers"], + "scopes": ["read:users", "read:roles:users", "admin:servers"], "services": ["tljh_repo2docker"], }, {