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

Make spinning up multiple Jupyterlabs configurable #499

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion jhub_apps/config_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from traitlets import Unicode, Union, List, Callable, Integer
from traitlets import Unicode, Union, List, Callable, Integer, Bool
from traitlets.config import SingletonConfigurable, Enum


Expand Down Expand Up @@ -48,3 +48,8 @@ class JAppsConfig(SingletonConfigurable):
2,
help="The number of workers to create for the JHub Apps FastAPI service",
).tag(config=True)

allow_multiple_jupyterlab = Bool(
False,
help="Allow users to spinup multiple JupyterLab servers",
).tag(config=True)
9 changes: 8 additions & 1 deletion jhub_apps/service/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@
get_spawner_profiles,
get_thumbnail_data_url,
get_shared_servers,
_check_multiple_jlab_allowed_if_framework_jlab,
)
from jhub_apps.service.app_from_git import _get_app_configuration_from_git
from jhub_apps.spawner.types import FRAMEWORKS
from jhub_apps.spawner.types import FRAMEWORKS, Framework
from jhub_apps.version import get_version

app = FastAPI()
Expand Down Expand Up @@ -147,6 +148,7 @@ async def create_server(
user: User = Depends(get_current_user),
):
# server.servername is not necessary to supply for create server
_check_multiple_jlab_allowed_if_framework_jlab(server.user_options)
server_name = server.user_options.display_name
logger.info("Creating server", server_name=server_name, user=user.name)
server.user_options.thumbnail = await get_thumbnail_data_url(
Expand Down Expand Up @@ -202,6 +204,7 @@ async def update_server(
user: User = Depends(get_current_user),
server_name=None,
):
_check_multiple_jlab_allowed_if_framework_jlab(server.user_options)
if thumbnail_data_url:
server.user_options.thumbnail = thumbnail_data_url
else:
Expand Down Expand Up @@ -245,8 +248,12 @@ async def me(user: User = Depends(get_current_user)):
@router.get("/frameworks/", description="Get all frameworks")
async def get_frameworks(user: User = Depends(get_current_user)):
logger.info("Getting all the frameworks")
config = get_jupyterhub_config()
frameworks = []
for framework in FRAMEWORKS:
is_jupyterlab = framework.name == Framework.jupyterlab.value
if is_jupyterlab and not config.JAppsConfig.allow_multiple_jupyterlab:
continue
frameworks.append(framework.json())
return frameworks

Expand Down
17 changes: 16 additions & 1 deletion jhub_apps/service/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
from cachetools import cached, TTLCache
from unittest.mock import Mock

from fastapi import HTTPException, status
from jupyterhub.app import JupyterHub
from traitlets.config import LazyConfigValue

from jhub_apps.hub_client.hub_client import HubClient
from jhub_apps.spawner.types import FrameworkConf, FRAMEWORKS_MAPPING
from jhub_apps.service.models import UserOptions
from jhub_apps.spawner.types import FrameworkConf, FRAMEWORKS_MAPPING, Framework
from slugify import slugify


Expand Down Expand Up @@ -168,3 +170,16 @@ def get_shared_servers(current_hub_user):
if server["name"] in shared_server_names
]
return shared_servers_rich


def _check_multiple_jlab_allowed_if_framework_jlab(user_options: UserOptions):
"""Checks if spinning up multiple JupyterLab servers per user is enabled, in case the selected
framework is JupyterLab.
"""
config = get_jupyterhub_config()
is_jupyterlab = user_options.framework == Framework.jupyterlab.value
if is_jupyterlab and not config.JAppsConfig.allow_multiple_jupyterlab:
raise HTTPException(
detail="Multiple JupyterLabs are not allowed on this deployment, please contact admin.",
status_code=status.HTTP_403_FORBIDDEN,
)
17 changes: 10 additions & 7 deletions jhub_apps/spawner/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ def values(cls):
return [member.value for role, member in cls.__members__.items()]


JUPYTERLAB_FRAMEWORK_CONFIG = FrameworkConf(
name=Framework.jupyterlab.value,
display_name="JupyterLab",
logo_path=STATIC_PATH.joinpath("jupyter.png"),
logo=f"{LOGO_BASE_PATH}/jupyter.png",
)



FRAMEWORKS = [
FrameworkConf(
name=Framework.panel.value,
Expand Down Expand Up @@ -76,18 +85,12 @@ def values(cls):
logo_path=STATIC_PATH.joinpath("gradio.png"),
logo=f"{LOGO_BASE_PATH}/gradio.png"
),
FrameworkConf(
name=Framework.jupyterlab.value,
display_name="JupyterLab",
logo_path=STATIC_PATH.joinpath("jupyter.png"),
logo=f"{LOGO_BASE_PATH}/jupyter.png",
),
FrameworkConf(
name=Framework.custom.value,
display_name="Custom Command",
logo_path=STATIC_PATH.joinpath("custom.png"),
logo=f"{LOGO_BASE_PATH}/custom.png"
),
JUPYTERLAB_FRAMEWORK_CONFIG,
]

FRAMEWORKS_MAPPING = {framework.name: framework for framework in FRAMEWORKS}
38 changes: 32 additions & 6 deletions jhub_apps/tests/tests_unit/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
from jhub_apps.hub_client.hub_client import HubClient
from jhub_apps.service.models import UserOptions, ServerCreation, Repository
from jhub_apps.service.utils import get_shared_servers
from jhub_apps.spawner.types import FRAMEWORKS
from jhub_apps.spawner.types import FRAMEWORKS, Framework, JUPYTERLAB_FRAMEWORK_CONFIG
from jhub_apps.tests.common.constants import MOCK_USER

frameworks_config_without_jupyterlab = [
f for f in FRAMEWORKS if f.name != Framework.jupyterlab.name
]

all_frameworks_config = frameworks_config_without_jupyterlab + [JUPYTERLAB_FRAMEWORK_CONFIG]


def mock_user_options():
user_options = {
Expand Down Expand Up @@ -60,9 +66,13 @@ def test_api_get_server_not_found(get_user, client):
}


@patch("jhub_apps.service.utils.get_jupyterhub_config")
@patch.object(HubClient, "create_server")
def test_api_create_server(create_server, client):
def test_api_create_server(create_server, get_jupyterhub_config, client):
from jhub_apps.service.models import UserOptions
get_jupyterhub_config.return_value = Mock(
JAppsConfig=Mock(allow_multiple_jupyterlab=True)
)
create_server_response = {"user": "jovyan"}
create_server.return_value = create_server_response
user_options = mock_user_options()
Expand Down Expand Up @@ -132,10 +142,13 @@ def test_api_delete_server(delete_server, name, remove, client):
assert response.json() == create_server_response


@patch("jhub_apps.service.utils.get_jupyterhub_config")
@patch.object(HubClient, "edit_server")
def test_api_update_server(edit_server, client):
def test_api_update_server(edit_server, get_jupyterhub_config, client):
from jhub_apps.service.models import UserOptions

get_jupyterhub_config.return_value = Mock(
JAppsConfig=Mock(allow_multiple_jupyterlab=True)
)
create_server_response = {"user": "jovyan"}
edit_server.return_value = create_server_response
user_options = mock_user_options()
Expand Down Expand Up @@ -196,12 +209,20 @@ def test_shared_server_filtering(hub_get_shared_servers, get_users):
get_users.assert_called_once_with()


def test_api_frameworks(client):
@pytest.mark.parametrize("allow_multiple_jupyterlab,frameworks_config", [
(True, all_frameworks_config),
(False, frameworks_config_without_jupyterlab)
])
@patch("jhub_apps.service.routes.get_jupyterhub_config")
def test_api_frameworks(get_jupyterhub_config, client, allow_multiple_jupyterlab, frameworks_config):
get_jupyterhub_config.return_value = Mock(
JAppsConfig=Mock(allow_multiple_jupyterlab=allow_multiple_jupyterlab)
)
response = client.get(
"/frameworks",
)
frameworks = []
for framework in FRAMEWORKS:
for framework in frameworks_config:
frameworks.append(framework.json())
assert response.json() == frameworks

Expand All @@ -225,11 +246,16 @@ def test_open_api_docs(client):
assert rjson['info']['version']


@patch("jhub_apps.service.utils.get_jupyterhub_config")
@patch.object(HubClient, "create_server")
def test_create_server_with_git_repository(
hub_create_server,
get_jupyterhub_config,
client,
):
get_jupyterhub_config.return_value = Mock(
JAppsConfig=Mock(allow_multiple_jupyterlab=True)
)
user_options = UserOptions(
jhub_app=True,
display_name="Test Application",
Expand Down
1 change: 1 addition & 0 deletions jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
c.JupyterHub.bind_url = hub_url
c.JAppsConfig.jupyterhub_config_path = "jupyterhub_config.py"
c.JAppsConfig.conda_envs = []
c.JAppsConfig.allow_multiple_jupyterlab = True
c.JAppsConfig.service_workers = 1
c.JupyterHub.default_url = "/hub/home"

Expand Down
Loading