From d343900947469d32d78558ef06b22e4b37e7850c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 25 Sep 2024 23:51:21 +0530 Subject: [PATCH] Spinup multiple JupyterLab servers (#486) * Spinup multiple JupyterLab servers * parametrize jupyterhub session * add test for server sharing * Skip sharing tests if jupyterhub<5 --- jhub_apps/hub_client/hub_client.py | 21 +++++++----- jhub_apps/spawner/types.py | 12 +++---- jhub_apps/tests/tests_e2e/test_api.py | 47 +++++++++++++++++++++++++-- jhub_apps/tests/tests_e2e/utils.py | 31 +++++++++++++----- 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/jhub_apps/hub_client/hub_client.py b/jhub_apps/hub_client/hub_client.py index c7224d86..9f3b3330 100644 --- a/jhub_apps/hub_client/hub_client.py +++ b/jhub_apps/hub_client/hub_client.py @@ -11,6 +11,7 @@ from jhub_apps.service.models import UserOptions, SharePermissions from jhub_apps.hub_client.utils import is_jupyterhub_5 +from jhub_apps.spawner.types import Framework API_URL = os.environ.get("JUPYTERHUB_API_URL") JUPYTERHUB_API_TOKEN = os.environ.get("JUPYTERHUB_API_TOKEN") @@ -183,15 +184,19 @@ def _create_server(self, username: str, servername: str, user_options: UserOptio logger.info("Creating new server", server_name=servername) r = requests.post(API_URL + url, headers=self._headers(), json=data) r.raise_for_status() - if is_jupyterhub_5(): - logger.info("Sharing", share_with=user_options.share_with) - self._share_server_with_multiple_entities( - username, - servername, - share_with=user_options.share_with - ) + if user_options.framework != Framework.jupyterlab.value: + if is_jupyterhub_5(): + logger.info("Sharing", share_with=user_options.share_with) + self._share_server_with_multiple_entities( + username, + servername, + share_with=user_options.share_with + ) + else: + logger.info("Not sharing server as JupyterHub < 5.x") else: - logger.info("Not sharing server as JupyterHub < 5.x") + logger.info(f"Not sharing the server as Framework is {user_options.framework}, " + f"sharing JupyterLab servers is not allowed.") return r.status_code, servername def _share_server( diff --git a/jhub_apps/spawner/types.py b/jhub_apps/spawner/types.py index eed5b8fd..2b868f9e 100644 --- a/jhub_apps/spawner/types.py +++ b/jhub_apps/spawner/types.py @@ -76,12 +76,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="", - # logo="", - # ), + 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", diff --git a/jhub_apps/tests/tests_e2e/test_api.py b/jhub_apps/tests/tests_e2e/test_api.py index fdaa8b27..2efd88d1 100644 --- a/jhub_apps/tests/tests_e2e/test_api.py +++ b/jhub_apps/tests/tests_e2e/test_api.py @@ -1,10 +1,13 @@ import hashlib +import uuid import pytest -from jhub_apps.service.models import Repository, UserOptions, ServerCreation +from jhub_apps.service.models import Repository, UserOptions, ServerCreation, SharePermissions +from jhub_apps.spawner.types import Framework from jhub_apps.tests.common.constants import JHUB_APPS_API_BASE_URL, JUPYTERHUB_HOSTNAME -from jhub_apps.tests.tests_e2e.utils import get_jhub_apps_session, fetch_url_until_title_found +from jhub_apps.tests.tests_e2e.utils import get_jhub_apps_session, fetch_url_until_title_found, \ + skip_if_jupyterhub_less_than_5 EXAMPLE_TEST_REPO = "https://github.com/nebari-dev/jhub-apps-from-git-repo-example.git" @@ -82,7 +85,7 @@ def test_create_server_with_git_repository(): jhub_app=True, display_name="Test Application", description="App description", - framework="panel", + framework=Framework.panel.value, thumbnail="data:image/png;base64,ZHVtbXkgaW1hZ2UgZGF0YQ==", filepath="panel_basic.py", repository=Repository( @@ -108,3 +111,41 @@ def test_create_server_with_git_repository(): fetch_url_until_title_found( session, url=created_app_url, expected_title="Panel Test App from Git Repository" ) + + +@skip_if_jupyterhub_less_than_5() +@pytest.mark.parametrize("framework, response_status_code,", [ + (Framework.panel.value, 200), + (Framework.jupyterlab.value, 403), +]) +def test_server_sharing(framework, response_status_code): + share_with_user = f"share-username-{uuid.uuid4().hex[:6]}" + shared_user_session = get_jhub_apps_session(username=share_with_user) + user_options = UserOptions( + jhub_app=True, + display_name="Test Application", + description="App description", + framework=framework, + thumbnail="data:image/png;base64,ZHVtbXkgaW1hZ2UgZGF0YQ==", + filepath="", + share_with=SharePermissions( + users=[share_with_user], + groups=[] + ) + ) + server_data = ServerCreation( + servername="test server sharing", + user_options=user_options + ) + data = {"data": server_data.model_dump_json()} + session = get_jhub_apps_session() + response = session.post( + f"{JHUB_APPS_API_BASE_URL}/server", + verify=False, + data=data, + ) + assert response.status_code == 200 + server_name = response.json()[-1] + created_app_url = f"http://{JUPYTERHUB_HOSTNAME}/user/admin/{server_name}/" + response = shared_user_session.get(created_app_url) + assert response.status_code == response_status_code diff --git a/jhub_apps/tests/tests_e2e/utils.py b/jhub_apps/tests/tests_e2e/utils.py index 3ca4225d..6abb1385 100644 --- a/jhub_apps/tests/tests_e2e/utils.py +++ b/jhub_apps/tests/tests_e2e/utils.py @@ -1,26 +1,36 @@ import time +import pytest import requests -from jhub_apps.tests.common.constants import JUPYTERHUB_HOSTNAME, JUPYTERHUB_USERNAME, JUPYTERHUB_PASSWORD +from jhub_apps.hub_client.utils import is_jupyterhub_5 +from jhub_apps.tests.common import constants -def get_jhub_apps_session(): +def get_jhub_apps_session(username=None): """Get jhub-apps session with authenticated cookies to be able to call jhub-apps API""" session = requests.Session() session.cookies.clear() + if username: + # Since we're using jupyterhub.auth.DummyAuthenticator, + # any pair of username/password will work fine. + login_username = username + login_password = username + else: + login_username = constants.JUPYTERHUB_USERNAME + login_password = constants.JUPYTERHUB_PASSWORD try: response = session.get( - f"http://{JUPYTERHUB_HOSTNAME}/hub/login", verify=False + f"http://{constants.JUPYTERHUB_HOSTNAME}/hub/login", verify=False ) response.raise_for_status() auth_data = { "_xsrf": session.cookies['_xsrf'], - "username": JUPYTERHUB_USERNAME, - "password": JUPYTERHUB_PASSWORD, + "username": login_username, + "password": login_password, } response = session.post( - f"http://{JUPYTERHUB_HOSTNAME}/hub/login?next=", + f"http://{constants.JUPYTERHUB_HOSTNAME}/hub/login?next=", headers={"Content-Type": "application/x-www-form-urlencoded"}, data=auth_data, verify=False, @@ -31,11 +41,11 @@ def get_jhub_apps_session(): raise ValueError(f"An error occurred during authentication: {e}") response_login = session.get( - f"http://{JUPYTERHUB_HOSTNAME}/services/japps/jhub-login", + f"http://{constants.JUPYTERHUB_HOSTNAME}/services/japps/jhub-login", ) response_login.raise_for_status() response_user = session.get( - f"http://{JUPYTERHUB_HOSTNAME}/services/japps/user", + f"http://{constants.JUPYTERHUB_HOSTNAME}/services/japps/user", verify=False ) response_user.raise_for_status() @@ -58,3 +68,8 @@ def fetch_url_until_title_found( if time_elapsed > timeout: raise TimeoutError(f"Failed to get the title {expected_title} within {timeout} seconds") from e time.sleep(interval) + + +def skip_if_jupyterhub_less_than_5(): + """Skip test if JupyterHub < 5""" + return pytest.mark.skipif(not is_jupyterhub_5(), reason="Skipping test because JupyterHub<5")