From 51b63766345f10a26d2105fe37b56cb286e5cbb8 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 27 Jan 2023 13:27:03 +0100 Subject: [PATCH 01/18] require sqlalchemy 1.4 removes some workarounds needed for sqlalchemy 1.1 + 2.0 support 1.4 backports most 2.0 behavior, keeping it off-by-default for an easier opt-in transition opt-in with `session.future = True` flag --- jupyterhub/orm.py | 33 ++++++++------------------------- jupyterhub/tests/test_db.py | 9 ++++++++- jupyterhub/tests/utils.py | 14 -------------- pytest.ini | 4 ++-- requirements.txt | 2 +- 5 files changed, 19 insertions(+), 43 deletions(-) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index d4c9672b..8b18bb0c 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -8,9 +8,7 @@ from datetime import datetime, timedelta import alembic.command import alembic.config -import sqlalchemy from alembic.script import ScriptDirectory -from packaging.version import parse as parse_version from sqlalchemy import ( Boolean, Column, @@ -31,18 +29,12 @@ from sqlalchemy import ( from sqlalchemy.orm import ( Session, backref, + declarative_base, interfaces, object_session, relationship, sessionmaker, ) - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - # sqlalchemy < 1.4 - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.pool import StaticPool from sqlalchemy.types import LargeBinary, Text, TypeDecorator from tornado.log import app_log @@ -912,27 +904,19 @@ def register_ping_connection(engine): @event.listens_for(engine, "engine_connect") def ping_connection(connection, branch=None): - if branch: - # "branch" refers to a sub-connection of a connection, - # we don't want to bother pinging on these. - return + # TODO: remove unused branch arg when we require sqlalchemy 2.0 # turn off "close with result". This flag is only used with # "connectionless" execution, otherwise will be False in any case save_should_close_with_result = connection.should_close_with_result connection.should_close_with_result = False - if parse_version(sqlalchemy.__version__) < parse_version("1.4"): - one = [1] - else: - one = 1 - try: # run a SELECT 1. use a core select() so that # the SELECT of a scalar value without a table is # appropriately formatted for the backend with connection.begin() as transaction: - connection.scalar(select(one)) + connection.scalar(select(1)) except exc.DBAPIError as err: # catch SQLAlchemy's DBAPIError, which is a wrapper # for the DBAPI's exception. It includes a .connection_invalidated @@ -948,7 +932,7 @@ def register_ping_connection(engine): # here also causes the whole connection pool to be invalidated # so that all stale connections are discarded. with connection.begin() as transaction: - connection.scalar(select(one)) + connection.scalar(select(1)) else: raise finally: @@ -972,11 +956,8 @@ def check_db_revision(engine): from .dbutil import _temp_alembic_ini - if hasattr(engine.url, "render_as_string"): - # sqlalchemy >= 1.4 - engine_url = engine.url.render_as_string(hide_password=False) - else: - engine_url = str(engine.url) + # alembic needs the password if it's in the URL + engine_url = engine.url.render_as_string(hide_password=False) with _temp_alembic_ini(engine_url) as ini: cfg = alembic.config.Config(ini) @@ -1067,6 +1048,8 @@ def new_session_factory( elif url.startswith('mysql'): kwargs.setdefault('pool_recycle', 60) + kwargs.setdefault("future", True) + if url.endswith(':memory:'): # If we're using an in-memory database, ensure that only one connection # is ever created. diff --git a/jupyterhub/tests/test_db.py b/jupyterhub/tests/test_db.py index 9c89abb6..ee9d0bbd 100644 --- a/jupyterhub/tests/test_db.py +++ b/jupyterhub/tests/test_db.py @@ -5,6 +5,7 @@ from glob import glob from subprocess import check_call import pytest +from packaging.version import parse as V from pytest import raises from traitlets.config import Config @@ -25,8 +26,14 @@ def generate_old_db(env_dir, hub_version, db_url): env_pip = os.path.join(env_dir, 'bin', 'pip') env_py = os.path.join(env_dir, 'bin', 'python') check_call([sys.executable, '-m', 'virtualenv', env_dir]) + pkgs = ['jupyterhub==' + hub_version] + # older jupyterhub needs older sqlachemy version - pkgs = ['jupyterhub==' + hub_version, 'sqlalchemy<1.4'] + if V(hub_version) < V("2"): + pkgs.append('sqlalchemy<1.4') + elif V(hub_version) < V("3.1.1"): + pkgs.append('sqlalchemy<2') + if 'mysql' in db_url: pkgs.append('mysql-connector-python') elif 'postgres' in db_url: diff --git a/jupyterhub/tests/utils.py b/jupyterhub/tests/utils.py index 6bae468d..54222658 100644 --- a/jupyterhub/tests/utils.py +++ b/jupyterhub/tests/utils.py @@ -14,20 +14,6 @@ from jupyterhub.objects import Server from jupyterhub.roles import assign_default_roles, update_roles from jupyterhub.utils import url_path_join as ujoin -try: - from sqlalchemy.exc import RemovedIn20Warning -except ImportError: - - class RemovedIn20Warning(DeprecationWarning): - """ - I only exist so I can be used in warnings filters in pytest.ini - - I will never be displayed. - - sqlalchemy 1.4 introduces RemovedIn20Warning, - but we still test against older sqlalchemy. - """ - class _AsyncRequests: """Wrapper around requests to return a Future from request methods diff --git a/pytest.ini b/pytest.ini index 1a57882e..b594142e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -20,5 +20,5 @@ markers = selenium: web tests that run with selenium filterwarnings = - error:.*:jupyterhub.tests.utils.RemovedIn20Warning - ignore:.*event listener has changed as of version 2.0.*:sqlalchemy.exc.SADeprecationWarning + ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning + ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SAWarning diff --git a/requirements.txt b/requirements.txt index 21de4a74..384e6a9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,6 @@ prometheus_client>=0.4.0 psutil>=5.6.5; sys_platform == 'win32' python-dateutil requests -SQLAlchemy>=1.1 +SQLAlchemy>=1.4 tornado>=5.1 traitlets>=4.3.2 From 11cd8674c258d59f7de6173ae742a65272f0e8d5 Mon Sep 17 00:00:00 2001 From: mouse1203 Date: Mon, 30 Jan 2023 12:54:54 +0100 Subject: [PATCH 02/18] selenium:add cases that covered Admin page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding cases: search, paging, start all/stop all, start/stop user´s server --- jupyterhub/tests/selenium/test_browser.py | 291 ++++++++++++++++++++++ 1 file changed, 291 insertions(+) diff --git a/jupyterhub/tests/selenium/test_browser.py b/jupyterhub/tests/selenium/test_browser.py index d86e8500..3b307541 100644 --- a/jupyterhub/tests/selenium/test_browser.py +++ b/jupyterhub/tests/selenium/test_browser.py @@ -990,3 +990,294 @@ async def test_oauth_page( # compare the scopes on the service page with the expected scope list assert sorted(authorized_scopes) == sorted(expected_scopes) + + +# ADMIN UI + + +async def open_admin_page(app, browser, user): + """function to open the admin page""" + admin_page = url_escape(app.base_url) + "hub/admin" + await open_url(app, browser, path="/login?next=" + admin_page) + await login(browser, user.name, pass_w=str(user.name)) + # wait for javascript to finish loading + await wait_for_ready(browser) + + +def create_admin_user(create_user_with_scopes): + # create user with appropriate access permissions + user_scopes = [ + "admin-ui", + "admin:users", + "admin:servers", + "admin:groups", + "access:servers", + "list:users", + ] + admin_user = create_user_with_scopes(*user_scopes) + return admin_user + + +def create_list_of_users(create_user_with_scopes, n): + return [create_user_with_scopes(["users"]) for i in range(1, n)] + + +async def test_open_admin_page(app, browser, create_user_with_scopes): + user = create_admin_user(create_user_with_scopes) + await open_admin_page(app, browser, user) + assert '/hub/admin' in browser.current_url + + +def check_btns(browser, name): + all_btns = browser.find_elements( + By.XPATH, '//*[@data-testid="user-row-server-activity"]//button' + ) + btns = list(filter(lambda x: name in x.get_attribute('class'), all_btns)) + return btns + + +async def test_start_stop_all_servers_on_admin_page( + app, browser, create_user_with_scopes +): + + user_admin = create_admin_user(create_user_with_scopes) + await open_admin_page(app, browser, user_admin) + # get total count of users from db + users_count_db = app.db.query(orm.User).count() + start_all_btn = browser.find_element( + By.XPATH, '//button[@type="button" and @data-testid="start-all"]' + ) + stop_all_btn = browser.find_element( + By.XPATH, '//button[@type="button" and @data-testid="stop-all"]' + ) + # verify Start All and Stop All buttons are displayed + assert start_all_btn.is_displayed(), stop_all_btn.is_displayed() + + async def click_all_btns(browser, btn_type, btn_await): + await click( + browser, + (By.XPATH, f'//button[@type="button" and @data-testid="{btn_type}"]'), + ) + await webdriver_wait( + browser, + EC.visibility_of_all_elements_located( + ( + By.XPATH, + '//*[@data-testid="user-row-server-activity"]//button[contains(@class, "%s")]' + % str(btn_await), + ) + ), + ) + + users = browser.find_elements(By.XPATH, '//td[@data-testid="user-row-name"]') + # verify that all servers are not started + # users´numbers are the same as numbers of the start button and the Spawn page button + # no Stop server buttons are displayed + # no access buttons are displayed + + class_names = ["stop-button", "primary", "start-button", "secondary"] + btns = {name: check_btns(browser, name) for name in class_names} + assert ( + len(btns["start-button"]) + == len(btns["secondary"]) + == len(users) + == users_count_db + ) + assert not btns["stop-button"] and not btns["primary"] + + # start all servers via the Start All + await click_all_btns(browser, "start-all", "stop-button") + # Start All and Stop All are still displayed + assert start_all_btn.is_displayed(), stop_all_btn.is_displayed() + + # users´numbers are the same as numbers of the stop button and the Access button + # no Start server buttons are displayed + # no Spawn page buttons are displayed + btns = {name: check_btns(browser, name) for name in class_names} + assert ( + len(btns["stop-button"]) == len(btns["primary"]) == len(users) == users_count_db + ) + assert not btns["start-button"] and not btns["secondary"] + + # stop all servers via the Stop All + await click_all_btns(browser, "stop-all", "start-button") + + # verify that all servers are stopped + # users´numbers are the same as numbers of the start button and the Spawn page button + # no Stop server buttons are displayed + # no access buttons are displayed + assert start_all_btn.is_displayed(), stop_all_btn.is_displayed() + btns = {name: check_btns(browser, name) for name in class_names} + assert ( + len(btns["start-button"]) + == len(btns["secondary"]) + == len(users) + == users_count_db + ) + assert not btns["stop-button"] and not btns["primary"] + + +async def test_paging_on_admin_page(app, browser, create_user_with_scopes, n=60): + admin_user = create_admin_user(create_user_with_scopes) + create_list_of_users(create_user_with_scopes, n) + await open_admin_page(app, browser, admin_user) + users = browser.find_elements(By.XPATH, '//td[@data-testid="user-row-name"]') + + # get total count of users from db + users_count_db = app.db.query(orm.User).count() + # get total count of users from UI page + users_list = [user.text for user in users] + displaying = browser.find_element( + By.XPATH, '//*[@class="pagination-footer"]//*[contains(text(),"Displaying")]' + ) + + btn_previous = browser.find_element( + By.XPATH, '//*[@class="pagination-footer"]//span[contains(text(),"Previous")]' + ) + btn_next = browser.find_element( + By.XPATH, '//*[@class="pagination-footer"]//span[contains(text(),"Next")]' + ) + + async def click_and_wait(browser, buttons_number): + await click( + browser, + ( + By.XPATH, + f'//*[@class="pagination-footer"]//button[contains(@class, "btn-light")][{buttons_number}]', + ), + ) + # wait for javascript to finish loading + await wait_for_ready(browser) + + assert f"0-{min(users_count_db,50)}" in displaying.text + if users_count_db > 50: + assert btn_next.get_dom_attribute("class") == "active-pagination" + # click on Next button + await click_and_wait(browser, buttons_number=2) + assert f"50-{users_count_db}" in displaying.text + assert btn_previous.get_dom_attribute("class") == "active-pagination" + # click on Previous button + await click_and_wait(browser, buttons_number=1) + else: + assert btn_previous.get_dom_attribute("class") == "inactive-pagination" + assert btn_next.get_dom_attribute("class") == "inactive-pagination" + + +async def test_search_on_admin_page( + app, browser, create_user_with_scopes, search_value="user", n=60 +): + admin_user = create_admin_user(create_user_with_scopes) + create_list_of_users(create_user_with_scopes, n) + await open_admin_page(app, browser, admin_user) + element_search = browser.find_element(By.XPATH, '//input[@name="user_search"]') + element_search.send_keys(search_value) + await asyncio.sleep(1) + # get the result of the search from db + users_count_db_filtered = ( + app.db.query(orm.User).filter(orm.User.name.like(f'%{search_value}%')).count() + ) + + filtered_list = browser.find_elements(By.XPATH, '//*[@class="user-row"]') + # check that count of users matches with number of users on the footer + displaying = browser.find_element( + By.XPATH, '//*[@class="pagination-footer"]//*[contains(text(),"Displaying")]' + ) + if users_count_db_filtered <= 50: + assert "0-" + str(users_count_db_filtered) in displaying.text + assert len(filtered_list) == users_count_db_filtered + else: + assert "0-50" in displaying.text + assert len(filtered_list) == 50 + # check that users names contain the search value in the filtered list + for element in filtered_list: + name = element.find_element( + By.XPATH, + '//*[@data-testid="user-row-name"]//span[contains(@data-testid, "user-name-div")]', + ) + assert search_value in name.text + + +async def test_start_stop_server_on_admin_page( + app, browser, create_user_with_scopes, n=5 +): + async def start_user(browser, expected_user): + start_button_xpath = f'//a[contains(@href, "spawn/{expected_user[0]}")]/preceding-sibling::button[contains(@class, "start-button")]' + await click(browser, (By.XPATH, start_button_xpath)) + start_btn = browser.find_element(By.XPATH, start_button_xpath) + await wait_for_ready(browser) + await webdriver_wait(browser, EC.staleness_of(start_btn)) + + async def spawn_user(browser, app, expected_user): + spawn_button_xpath = f'//a[contains(@href, "spawn/{expected_user[1]}")]/button[contains(@class, "secondary")]' + await click(browser, (By.XPATH, spawn_button_xpath)) + while ( + not app.users[1].spawner.ready + and f"/hub/spawn-pending/{expected_user[1]}" in browser.current_url + ): + await webdriver_wait(browser, EC.url_contains(f"/user/{expected_user[1]}/")) + + async def access_srv_user(browser, expected_user): + access_buttons_xpath = '//*[@data-testid="user-row-server-activity"]//button[contains(@class, "primary")]' + for i, ex_user in enumerate(expected_user): + access_btn_xpath = f'//a[contains(@href, "user/{expected_user[i]}")]/button[contains(@class, "primary")]' + await click(browser, (By.XPATH, access_btn_xpath)) + if not f"/user/{expected_user[i]}/" in browser.current_url: + await webdriver_wait( + browser, EC.url_contains(f"/user/{expected_user[i]}/") + ) + browser.back() + + async def stop_srv_users(browser, expected_user): + for i, ex_user in enumerate(expected_user): + stop_btn_xpath = f'//a[contains(@href, "user/{expected_user[i]}")]/preceding-sibling::button[contains(@class, "stop-button")]' + stop_btn = browser.find_element(By.XPATH, stop_btn_xpath) + await click(browser, (By.XPATH, stop_btn_xpath)) + await webdriver_wait(browser, EC.staleness_of(stop_btn)) + + user_admin = create_admin_user(create_user_with_scopes) + create_list_of_users(create_user_with_scopes, n) + await open_admin_page(app, browser, user_admin) + users = browser.find_elements(By.XPATH, '//td[@data-testid="user-row-name"]') + users_list = [user.text for user in users] + expected_user = [users_list[n - 3], users_list[n - 4]] + spawn_page_btns = browser.find_elements( + By.XPATH, + '//*[@data-testid="user-row-server-activity"]//a[contains(@href, "spawn/")]', + ) + + for i, user in enumerate(users): + spawn_page_btn = spawn_page_btns[i] + user_from_table = user.text + link = spawn_page_btn.get_attribute('href') + assert f"/spawn/{user_from_table}" in link + + # click on Start button + await start_user(browser, expected_user) + class_names = ["stop-button", "primary", "start-button", "secondary"] + btns = {name: check_btns(browser, name) for name in class_names} + assert len(btns["stop-button"]) == 1 + + # click on Spawn page button + await spawn_user(browser, app, expected_user) + assert f"/user/{expected_user[1]}/" in browser.current_url + + # open the Admin page + await open_url(app, browser, "/admin") + # wait for javascript to finish loading + await wait_for_ready(browser) + assert "/hub/admin" in browser.current_url + btns = {name: check_btns(browser, name) for name in class_names} + assert len(btns["stop-button"]) == len(btns["primary"]) == 2 + + # click on the Access button + await access_srv_user(browser, expected_user) + + assert "/hub/admin" in browser.current_url + btns = {name: check_btns(browser, name) for name in class_names} + assert len(btns["stop-button"]) == 2 + + # click on Stop button for both users + await stop_srv_users(browser, expected_user) + btns = {name: check_btns(browser, name) for name in class_names} + assert len(btns["stop-button"]) == 0 + assert len(btns["primary"]) == 0 From 9884fa7127d2618c0b452d06ec1cfad1fc2848d4 Mon Sep 17 00:00:00 2001 From: mouse1203 Date: Mon, 30 Jan 2023 14:50:57 +0100 Subject: [PATCH 03/18] selenium: update spawn_pending_server_ready adding await wait_for_ready(browser) replacing await in_thread(browser.get, home_page) adding waiting start all/stop all buttons in case start_stop_all_servers_on_admin_page --- jupyterhub/tests/selenium/test_browser.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/jupyterhub/tests/selenium/test_browser.py b/jupyterhub/tests/selenium/test_browser.py index 3b307541..c95859db 100644 --- a/jupyterhub/tests/selenium/test_browser.py +++ b/jupyterhub/tests/selenium/test_browser.py @@ -375,9 +375,10 @@ async def test_spawn_pending_server_ready(app, browser, user): await webdriver_wait(browser, EC.staleness_of(button_start)) # checking that server is running and two butons present on the home page home_page = url_path_join(public_host(app), ujoin(app.base_url, "hub/home")) - await in_thread(browser.get, home_page) while not user.spawner.ready: await asyncio.sleep(0.01) + await in_thread(browser.get, home_page) + await wait_for_ready(browser) assert is_displayed(browser, (By.ID, "stop")) assert is_displayed(browser, (By.ID, "start")) @@ -1044,12 +1045,25 @@ async def test_start_stop_all_servers_on_admin_page( await open_admin_page(app, browser, user_admin) # get total count of users from db users_count_db = app.db.query(orm.User).count() + await webdriver_wait( + browser, + lambda: is_displayed( + By.XPATH, '//button[@type="button" and @data-testid="start-all"]' + ), + ) + await webdriver_wait( + browser, + lambda: is_displayed( + By.XPATH, '//button[@type="button" and @data-testid="stop-all"]' + ), + ) start_all_btn = browser.find_element( By.XPATH, '//button[@type="button" and @data-testid="start-all"]' ) stop_all_btn = browser.find_element( By.XPATH, '//button[@type="button" and @data-testid="stop-all"]' ) + # verify Start All and Stop All buttons are displayed assert start_all_btn.is_displayed(), stop_all_btn.is_displayed() From d5bc135d9b7da9d5e92a037a6cd5cb279e9b933d Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 30 Jan 2023 15:42:07 +0100 Subject: [PATCH 04/18] fix xsrfToken in react caught now that we have browser tests --- jsx/src/util/jhapiUtil.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsx/src/util/jhapiUtil.js b/jsx/src/util/jhapiUtil.js index 8c8bb005..99861057 100644 --- a/jsx/src/util/jhapiUtil.js +++ b/jsx/src/util/jhapiUtil.js @@ -8,7 +8,7 @@ export const jhapiRequest = (endpoint, method, data) => { if (xsrfToken) { // add xsrf token to url parameter var sep = endpoint.indexOf("?") === -1 ? "?" : "&"; - suffix = sep + "_xsrf=" + xsrf_token; + suffix = sep + "_xsrf=" + xsrfToken; } return fetch(api_url + endpoint + suffix, { method: method, From bc751b07400072c5441227e30bf516071cbf7c4a Mon Sep 17 00:00:00 2001 From: mouse1203 Date: Tue, 31 Jan 2023 13:18:47 +0100 Subject: [PATCH 05/18] Selenium: add wait element into open_admin_page adding a waiting for loading of element --- jupyterhub/tests/selenium/test_browser.py | 29 +++++++++-------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/jupyterhub/tests/selenium/test_browser.py b/jupyterhub/tests/selenium/test_browser.py index c95859db..a25e669d 100644 --- a/jupyterhub/tests/selenium/test_browser.py +++ b/jupyterhub/tests/selenium/test_browser.py @@ -1001,8 +1001,13 @@ async def open_admin_page(app, browser, user): admin_page = url_escape(app.base_url) + "hub/admin" await open_url(app, browser, path="/login?next=" + admin_page) await login(browser, user.name, pass_w=str(user.name)) - # wait for javascript to finish loading - await wait_for_ready(browser) + # waiting for loading of admin page elements + await webdriver_wait( + browser, + lambda browser: is_displayed( + browser, (By.XPATH, '//div[@class="resets"]/div[@data-testid="container"]') + ), + ) def create_admin_user(create_user_with_scopes): @@ -1024,8 +1029,8 @@ def create_list_of_users(create_user_with_scopes, n): async def test_open_admin_page(app, browser, create_user_with_scopes): - user = create_admin_user(create_user_with_scopes) - await open_admin_page(app, browser, user) + admin_user = create_admin_user(create_user_with_scopes) + await open_admin_page(app, browser, admin_user) assert '/hub/admin' in browser.current_url @@ -1045,18 +1050,7 @@ async def test_start_stop_all_servers_on_admin_page( await open_admin_page(app, browser, user_admin) # get total count of users from db users_count_db = app.db.query(orm.User).count() - await webdriver_wait( - browser, - lambda: is_displayed( - By.XPATH, '//button[@type="button" and @data-testid="start-all"]' - ), - ) - await webdriver_wait( - browser, - lambda: is_displayed( - By.XPATH, '//button[@type="button" and @data-testid="stop-all"]' - ), - ) + start_all_btn = browser.find_element( By.XPATH, '//button[@type="button" and @data-testid="start-all"]' ) @@ -1153,6 +1147,7 @@ async def test_paging_on_admin_page(app, browser, create_user_with_scopes, n=60) ) async def click_and_wait(browser, buttons_number): + # number 1 - previous button, number 2 - next button await click( browser, ( @@ -1160,8 +1155,6 @@ async def test_paging_on_admin_page(app, browser, create_user_with_scopes, n=60) f'//*[@class="pagination-footer"]//button[contains(@class, "btn-light")][{buttons_number}]', ), ) - # wait for javascript to finish loading - await wait_for_ready(browser) assert f"0-{min(users_count_db,50)}" in displaying.text if users_count_db > 50: From f857b1702278e55baee6c5dfd4a025a4f49c69ce Mon Sep 17 00:00:00 2001 From: alwasega Date: Fri, 3 Feb 2023 15:57:35 +0300 Subject: [PATCH 06/18] Moved last set of Tutorials --- docs/source/changelog.md | 2 ++ docs/source/getting-started/index.md | 17 ---------- docs/source/index.md | 7 ---- docs/source/reference/index.md | 3 +- docs/source/reference/technical-overview.md | 2 +- .../{reference => tutorial/api}/server-api.md | 6 ++-- .../authenticators-users-basics.md | 0 .../getting-started/config-basics.md | 2 +- .../getting-started/networking-basics.md | 0 .../getting-started/security-basics.md | 0 .../getting-started/services-basics.md | 10 +++--- .../getting-started/spawners-basics.md | 0 docs/source/tutorial/index.md | 32 +++++++++++++++++-- 13 files changed, 44 insertions(+), 37 deletions(-) delete mode 100644 docs/source/getting-started/index.md rename docs/source/{reference => tutorial/api}/server-api.md (97%) rename docs/source/{ => tutorial}/getting-started/authenticators-users-basics.md (100%) rename docs/source/{ => tutorial}/getting-started/config-basics.md (98%) rename docs/source/{ => tutorial}/getting-started/networking-basics.md (100%) rename docs/source/{ => tutorial}/getting-started/security-basics.md (100%) rename docs/source/{ => tutorial}/getting-started/services-basics.md (93%) rename docs/source/{ => tutorial}/getting-started/spawners-basics.md (100%) diff --git a/docs/source/changelog.md b/docs/source/changelog.md index 86192a46..523a6b77 100644 --- a/docs/source/changelog.md +++ b/docs/source/changelog.md @@ -1,3 +1,5 @@ +(changelog)= + # Changelog For detailed changes from the prior release, click on the version number, and diff --git a/docs/source/getting-started/index.md b/docs/source/getting-started/index.md deleted file mode 100644 index d02a3741..00000000 --- a/docs/source/getting-started/index.md +++ /dev/null @@ -1,17 +0,0 @@ -# Get Started - -This section covers how to configure and customize JupyterHub for your -needs. It contains information about authentication, networking, security, and -other topics that are relevant to individuals or organizations deploying their -own JupyterHub. - -```{toctree} -:maxdepth: 2 - -config-basics -networking-basics -security-basics -authenticators-users-basics -spawners-basics -services-basics -``` diff --git a/docs/source/index.md b/docs/source/index.md index 6f468935..f62e5830 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -81,13 +81,6 @@ Today, you can find two main use cases: _It is important to evaluate these distributions before you can continue with the configuration of JupyterHub_. -### Getting Started - -```{toctree} -:maxdepth: 2 - -getting-started/index -``` ### Technical Reference diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md index c7866360..5a253089 100644 --- a/docs/source/reference/index.md +++ b/docs/source/reference/index.md @@ -1,3 +1,5 @@ +(reference-index)= + # Technical Reference This section covers more of the details of the JupyterHub architecture, as well as @@ -13,7 +15,6 @@ authenticators spawners services rest-api -server-api monitoring database ../events/index diff --git a/docs/source/reference/technical-overview.md b/docs/source/reference/technical-overview.md index 9ce3f4f2..79e0f8c3 100644 --- a/docs/source/reference/technical-overview.md +++ b/docs/source/reference/technical-overview.md @@ -110,7 +110,7 @@ working directory: This file needs to persist so that a **Hub** server restart will avoid invalidating cookies. Conversely, deleting this file and restarting the server effectively invalidates all login cookies. The cookie secret file is discussed - in the [Cookie Secret section of the Security Settings document](../getting-started/security-basics.md). + in the [Cookie Secret section of the Security Settings document](security-basics). The location of these files can be specified via configuration settings. It is recommended that these files be stored in standard UNIX filesystem locations, diff --git a/docs/source/reference/server-api.md b/docs/source/tutorial/api/server-api.md similarity index 97% rename from docs/source/reference/server-api.md rename to docs/source/tutorial/api/server-api.md index e87530d0..4e8fa1f6 100644 --- a/docs/source/reference/server-api.md +++ b/docs/source/tutorial/api/server-api.md @@ -252,7 +252,7 @@ data: {"progress": 100, "ready": true, "message": "Server ready at /user/test-us Here is a Python example for consuming an event stream: -```{literalinclude} ../../../examples/server-api/start-stop-server.py +```{literalinclude} ../../../../examples/server-api/start-stop-server.py :language: python :pyobject: event_stream ``` @@ -285,7 +285,7 @@ The only way to wait for a server to stop is to poll it and wait for the server This Python code snippet can be used to stop a server and the wait for the process to complete: -```{literalinclude} ../../../examples/server-api/start-stop-server.py +```{literalinclude} ../../../../examples/server-api/start-stop-server.py :language: python :pyobject: stop_server ``` @@ -325,7 +325,7 @@ In summary, the processes involved in managing servers on behalf of users are: The example below demonstrates starting and stopping servers via the JupyterHub API, including waiting for them to start via the progress API and waiting for them to stop by polling the user model. -```{literalinclude} ../../../examples/server-api/start-stop-server.py +```{literalinclude} ../../../../examples/server-api/start-stop-server.py :language: python :start-at: def event_stream :end-before: def main diff --git a/docs/source/getting-started/authenticators-users-basics.md b/docs/source/tutorial/getting-started/authenticators-users-basics.md similarity index 100% rename from docs/source/getting-started/authenticators-users-basics.md rename to docs/source/tutorial/getting-started/authenticators-users-basics.md diff --git a/docs/source/getting-started/config-basics.md b/docs/source/tutorial/getting-started/config-basics.md similarity index 98% rename from docs/source/getting-started/config-basics.md rename to docs/source/tutorial/getting-started/config-basics.md index 0b22a995..3a9f0ca4 100644 --- a/docs/source/getting-started/config-basics.md +++ b/docs/source/tutorial/getting-started/config-basics.md @@ -1,7 +1,7 @@ # Configuration Basics This section contains basic information about configuring settings for a JupyterHub -deployment. The [Technical Reference](../reference/index) +deployment. The [Technical Reference](reference-index) documentation provides additional details. This section will help you learn how to: diff --git a/docs/source/getting-started/networking-basics.md b/docs/source/tutorial/getting-started/networking-basics.md similarity index 100% rename from docs/source/getting-started/networking-basics.md rename to docs/source/tutorial/getting-started/networking-basics.md diff --git a/docs/source/getting-started/security-basics.md b/docs/source/tutorial/getting-started/security-basics.md similarity index 100% rename from docs/source/getting-started/security-basics.md rename to docs/source/tutorial/getting-started/security-basics.md diff --git a/docs/source/getting-started/services-basics.md b/docs/source/tutorial/getting-started/services-basics.md similarity index 93% rename from docs/source/getting-started/services-basics.md rename to docs/source/tutorial/getting-started/services-basics.md index 5ec3f779..ae23912e 100644 --- a/docs/source/getting-started/services-basics.md +++ b/docs/source/tutorial/getting-started/services-basics.md @@ -14,7 +14,7 @@ document will: - explain some basic information about API tokens - clarify that API tokens can be used to authenticate to - single-user servers as of [version 0.8.0](../changelog) + single-user servers as of [version 0.8.0](changelog) - show how the [jupyterhub_idle_culler][] script can be: - used in a Hub-managed service - run as a standalone script @@ -29,19 +29,19 @@ Hub via the REST API. To run such an external service, an API token must be created and provided to the service. -As of [version 0.6.0](../changelog), the preferred way of doing +As of [version 0.6.0](changelog), the preferred way of doing this is to first generate an API token: ```bash openssl rand -hex 32 ``` -In [version 0.8.0](../changelog), a TOKEN request page for +In [version 0.8.0](changelog), a TOKEN request page for generating an API token is available from the JupyterHub user interface: -![Request API TOKEN page](../images/token-request.png) +![Request API TOKEN page](/images/token-request.png) -![API TOKEN success page](../images/token-request-success.png) +![API TOKEN success page](/images/token-request-success.png) ### Step 2: Pass environment variable with token to the Hub diff --git a/docs/source/getting-started/spawners-basics.md b/docs/source/tutorial/getting-started/spawners-basics.md similarity index 100% rename from docs/source/getting-started/spawners-basics.md rename to docs/source/tutorial/getting-started/spawners-basics.md diff --git a/docs/source/tutorial/index.md b/docs/source/tutorial/index.md index 9fa671d0..490dd00f 100644 --- a/docs/source/tutorial/index.md +++ b/docs/source/tutorial/index.md @@ -1,10 +1,10 @@ # Tutorials -This section of the documentation provides step-by-step tutorials to help you achieve a specific goal. The tutorials should be a good place to start learning about JupyterHub and how it works. +The *Tutorials* provide step-by-step lessons to help you achieve a specific goal. The tutorials should be a good place to start learning about JupyterHub and how it works. ## Installation -These sections cover how to get up-and-running with JupyterHub. They cover +This section covers how to get up-and-running with JupyterHub. It covers some basics of the tools needed to deploy JupyterHub as well as how to get it running on your own infrastructure. @@ -15,3 +15,31 @@ installation/quickstart installation/installation-basics installation/quickstart-docker ``` + +## Getting Started + +This section covers how to configure and customize JupyterHub for your +needs. It contains information about authentication, networking, security, and +other topics that are relevant to individuals or organizations deploying their +own JupyterHub. + +```{toctree} +:maxdepth: 1 + +getting-started/config-basics +getting-started/networking-basics +getting-started/security-basics +getting-started/authenticators-users-basics +getting-started/services-basics +getting-started/spawners-basics +``` + +## Working with the JupyterHub API + +JupyterHub's functionalities can be accessed using its API. In this section, we cover how to use the JupyterHub API to achieve specific goals, for example, starting servers. + +```{toctree} +:maxdepth: 1 + +api/server-api +``` \ No newline at end of file From 195ec4c359a32e9a4e9bda0dfb89906431275701 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Feb 2023 13:02:02 +0000 Subject: [PATCH 07/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/index.md | 1 - docs/source/tutorial/index.md | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/source/index.md b/docs/source/index.md index f62e5830..83be428e 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -81,7 +81,6 @@ Today, you can find two main use cases: _It is important to evaluate these distributions before you can continue with the configuration of JupyterHub_. - ### Technical Reference ```{toctree} diff --git a/docs/source/tutorial/index.md b/docs/source/tutorial/index.md index 490dd00f..fc9b1bd7 100644 --- a/docs/source/tutorial/index.md +++ b/docs/source/tutorial/index.md @@ -1,6 +1,6 @@ # Tutorials -The *Tutorials* provide step-by-step lessons to help you achieve a specific goal. The tutorials should be a good place to start learning about JupyterHub and how it works. +The _Tutorials_ provide step-by-step lessons to help you achieve a specific goal. The tutorials should be a good place to start learning about JupyterHub and how it works. ## Installation @@ -36,10 +36,10 @@ getting-started/spawners-basics ## Working with the JupyterHub API -JupyterHub's functionalities can be accessed using its API. In this section, we cover how to use the JupyterHub API to achieve specific goals, for example, starting servers. +JupyterHub's functionalities can be accessed using its API. In this section, we cover how to use the JupyterHub API to achieve specific goals, for example, starting servers. ```{toctree} :maxdepth: 1 api/server-api -``` \ No newline at end of file +``` From d1fba40f9a770c95848b8f0f0fcb664f6146eb1f Mon Sep 17 00:00:00 2001 From: alwasega Date: Fri, 3 Feb 2023 16:54:24 +0300 Subject: [PATCH 08/18] Added suggestion in tutorials/index.md --- docs/source/tutorial/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorial/index.md b/docs/source/tutorial/index.md index fc9b1bd7..f4c80d88 100644 --- a/docs/source/tutorial/index.md +++ b/docs/source/tutorial/index.md @@ -1,6 +1,6 @@ # Tutorials -The _Tutorials_ provide step-by-step lessons to help you achieve a specific goal. The tutorials should be a good place to start learning about JupyterHub and how it works. +_Tutorials_ provide step-by-step lessons to help you achieve a specific goal. They should be a good place to start learning about JupyterHub and how it works. ## Installation From f2ac996bc6a87f606f90778fe583ab6531953d0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 05:06:42 +0000 Subject: [PATCH 09/18] build(deps): bump docker/build-push-action from 3.3.0 to 4.0.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.3.0 to 4.0.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/37abcedcc1da61a57767b7588cb9d03eb57e28b3...3b5e8027fcad23fda98b2e3ac259d8d67585f671) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3c59811..e44e7f54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,7 +149,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub - uses: docker/build-push-action@37abcedcc1da61a57767b7588cb9d03eb57e28b3 + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 with: context: . platforms: linux/amd64,linux/arm64 @@ -170,7 +170,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub-onbuild - uses: docker/build-push-action@37abcedcc1da61a57767b7588cb9d03eb57e28b3 + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 with: build-args: | BASE_IMAGE=${{ fromJson(steps.jupyterhubtags.outputs.tags)[0] }} @@ -191,7 +191,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub-demo - uses: docker/build-push-action@37abcedcc1da61a57767b7588cb9d03eb57e28b3 + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 with: build-args: | BASE_IMAGE=${{ fromJson(steps.onbuildtags.outputs.tags)[0] }} @@ -215,7 +215,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub/singleuser - uses: docker/build-push-action@37abcedcc1da61a57767b7588cb9d03eb57e28b3 + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 with: build-args: | JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }} From c4d576260847d592b9a2d6a7735a614285e84d06 Mon Sep 17 00:00:00 2001 From: alwasega Date: Mon, 6 Feb 2023 11:31:37 +0300 Subject: [PATCH 10/18] Moved Explanation/Background files --- .../about}/contributor-list.md | 0 .../about}/gallery-jhub-deployments.md | 0 .../admin/capacity-planning.md | 6 +-- .../admin}/database.md | 4 +- .../{reference => background/admin}/oauth.md | 2 +- .../admin}/websecurity.md | 2 + docs/source/background/index.md | 37 +++++++++++++++++++ docs/source/changelog.md | 4 +- docs/source/contributing/security.md | 2 +- docs/source/contributing/setup.md | 2 +- docs/source/explanation/index.md | 8 ---- docs/source/index-about.md | 14 ------- docs/source/index-admin.md | 11 ------ docs/source/index.md | 24 +----------- docs/source/rbac/index.md | 2 +- docs/source/rbac/scopes.md | 6 ++- docs/source/rbac/tech-implementation.md | 4 +- docs/source/rbac/upgrade.md | 2 +- docs/source/rbac/use-cases.md | 4 +- docs/source/reference/index.md | 3 -- docs/source/reference/urls.md | 2 + 21 files changed, 63 insertions(+), 76 deletions(-) rename docs/source/{ => background/about}/contributor-list.md (100%) rename docs/source/{ => background/about}/gallery-jhub-deployments.md (100%) rename docs/source/{ => background}/admin/capacity-planning.md (99%) rename docs/source/{reference => background/admin}/database.md (98%) rename docs/source/{reference => background/admin}/oauth.md (99%) rename docs/source/{reference => background/admin}/websecurity.md (99%) create mode 100644 docs/source/background/index.md delete mode 100644 docs/source/explanation/index.md delete mode 100644 docs/source/index-about.md delete mode 100644 docs/source/index-admin.md diff --git a/docs/source/contributor-list.md b/docs/source/background/about/contributor-list.md similarity index 100% rename from docs/source/contributor-list.md rename to docs/source/background/about/contributor-list.md diff --git a/docs/source/gallery-jhub-deployments.md b/docs/source/background/about/gallery-jhub-deployments.md similarity index 100% rename from docs/source/gallery-jhub-deployments.md rename to docs/source/background/about/gallery-jhub-deployments.md diff --git a/docs/source/admin/capacity-planning.md b/docs/source/background/admin/capacity-planning.md similarity index 99% rename from docs/source/admin/capacity-planning.md rename to docs/source/background/admin/capacity-planning.md index ef0524cd..4b122b1b 100644 --- a/docs/source/admin/capacity-planning.md +++ b/docs/source/background/admin/capacity-planning.md @@ -40,7 +40,7 @@ The rest is going to be up to your users. Per-user overhead from JupyterHub is typically negligible up to at least a few hundred concurrent active users. -```{figure} ../images/mybinder-hub-components-cpu-memory.png +```{figure} /images/mybinder-hub-components-cpu-memory.png JupyterHub component resource usage for mybinder.org. ``` @@ -200,7 +200,7 @@ The limit here is actually Kubernetes' pods per node, not memory _or_ CPU. This is likely a extreme case, as many Binder users come from clicking links on webpages without any actual intention of running code. -```{figure} ../images/mybinder-load5.png +```{figure} /images/mybinder-load5.png mybinder.org node CPU usage is low with 50-150 users sharing just 8 cores ``` @@ -277,7 +277,7 @@ showing >90% of users using less than 10% CPU and 200MB, but a few outliers near the limit of 1 CPU and 2GB of RAM. This is the kind of information you can use to tune your requests and limits. -![Snapshot from JupyterHub's Grafana dashboards on mybinder.org](../images/mybinder-user-resources.png) +![Snapshot from JupyterHub's Grafana dashboards on mybinder.org](/images/mybinder-user-resources.png) [prometheus]: https://prometheus.io [grafana]: https://grafana.com diff --git a/docs/source/reference/database.md b/docs/source/background/admin/database.md similarity index 98% rename from docs/source/reference/database.md rename to docs/source/background/admin/database.md index edc26056..c3272503 100644 --- a/docs/source/reference/database.md +++ b/docs/source/background/admin/database.md @@ -1,3 +1,5 @@ +(hub-database)= + # The Hub's Database JupyterHub uses a database to store information about users, services, and other data needed for operating the Hub. @@ -80,7 +82,7 @@ Additionally, there is usually _very_ little load on the database itself. By far the most taxing activity on the database is the 'list all users' endpoint, primarily used by the [idle-culling service](https://github.com/jupyterhub/jupyterhub-idle-culler). Database-based optimizations have been added to make even these operations feasible for large numbers of users: -1. State filtering on [GET /users](./rest-api.md) with `?state=active`, +1. State filtering on [GET /users](jupyterhub-rest-API) with `?state=active`, which limits the number of results in the query to only the relevant subset (added in JupyterHub 1.3), rather than all users. 2. [Pagination](api-pagination) of all list endpoints, allowing the request of a large number of resources to be more fairly balanced with other Hub activities across multiple requests (added in 2.0). diff --git a/docs/source/reference/oauth.md b/docs/source/background/admin/oauth.md similarity index 99% rename from docs/source/reference/oauth.md rename to docs/source/background/admin/oauth.md index ab0aeb86..edc2aea6 100644 --- a/docs/source/reference/oauth.md +++ b/docs/source/background/admin/oauth.md @@ -255,7 +255,7 @@ To authenticate this request, the single token stored in the encrypted cookie is If the user model matches who should be allowed (e.g. Danez), then the request is allowed. -See {doc}`../rbac/scopes` for how JupyterHub uses scopes to determine authorized access to servers and services. +See [Scopes in JupyterHub](jupyterhub-scopes) for how JupyterHub uses scopes to determine authorized access to servers and services. _the end_ diff --git a/docs/source/reference/websecurity.md b/docs/source/background/admin/websecurity.md similarity index 99% rename from docs/source/reference/websecurity.md rename to docs/source/background/admin/websecurity.md index 8ff2cc23..d4970187 100644 --- a/docs/source/reference/websecurity.md +++ b/docs/source/background/admin/websecurity.md @@ -1,3 +1,5 @@ +(web-security)= + # Security Overview The **Security Overview** section helps you learn about: diff --git a/docs/source/background/index.md b/docs/source/background/index.md new file mode 100644 index 00000000..3a446aab --- /dev/null +++ b/docs/source/background/index.md @@ -0,0 +1,37 @@ +# Background + +_Background_ provides some basic information about JupyterHub and big-picture illustrations of how it works. This section is meant to build your understanding of particular topics and JupyterHub in general. + +## About JupyterHub +JupyterHub is an open source project and community. It is a part of the +[Jupyter Project](https://jupyter.org). This section covers information +about our community as well as institutions that are presently using JupyterHub. + +```{toctree} +:maxdepth: 1 + +about/gallery-jhub-deployments +../changelog +about/contributor-list +``` + +## Administration +This section provides information relevant to running your own JupyterHub over time. + +```{toctree} +:maxdepth: 1 + +admin/capacity-planning +admin/database +admin/websecurity +admin/oauth +``` + +## JupyterHub RBAC +This section covers how Role Based Access Control (RBAC) is implemented in JupyterHub to control access to Jupyterhub's API resources. + +```{toctree} +:maxdepth: 2 + +../rbac/index +``` \ No newline at end of file diff --git a/docs/source/changelog.md b/docs/source/changelog.md index 523a6b77..4c1a8191 100644 --- a/docs/source/changelog.md +++ b/docs/source/changelog.md @@ -207,7 +207,7 @@ More info in {ref}`available-scopes-target`. - The admin UI can now show more detailed info about users and their servers in a drop-down details table: - ![Details view in admin UI](./images/dropdown-details-3.0.png) + ![Details view in admin UI](/images/dropdown-details-3.0.png) - Several bugfixes and improvements in the new admin UI. - Direct access to the Hub's database is deprecated. @@ -1355,7 +1355,7 @@ whether it was through discussion, testing, documentation, or development. - There is now full UI support for managing named servers. With named servers, each jupyterhub user may have access to more than one named server. For example, a professor may access a server named `research` and another named `teaching`. - ![named servers on the home page](./images/named-servers-home.png) + ![named servers on the home page](/images/named-servers-home.png) - Authenticators can now expire and refresh authentication data by implementing `Authenticator.refresh_user(user)`. diff --git a/docs/source/contributing/security.md b/docs/source/contributing/security.md index 0589c65d..66dc294d 100644 --- a/docs/source/contributing/security.md +++ b/docs/source/contributing/security.md @@ -1,7 +1,7 @@ # Reporting security issues in Jupyter or JupyterHub If you find a security vulnerability in Jupyter or JupyterHub, -whether it is a failure of the security model described in {doc}`../reference/websecurity` +whether it is a failure of the security model described in [Security Overview](web-security) or a failure in implementation, please report it to . diff --git a/docs/source/contributing/setup.md b/docs/source/contributing/setup.md index 0bde832f..def3bc24 100644 --- a/docs/source/contributing/setup.md +++ b/docs/source/contributing/setup.md @@ -103,7 +103,7 @@ a more detailed discussion. The default database engine is `sqlite` so if you are just trying to get up and running quickly for local development that should be available via [Python](https://docs.python.org/3.5/library/sqlite3.html). - See {doc}`/reference/database` for details on other supported databases. + See [The Hub's Database](hub-database) for details on other supported databases. 6. You are now ready to start JupyterHub! diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md deleted file mode 100644 index 07dbcc86..00000000 --- a/docs/source/explanation/index.md +++ /dev/null @@ -1,8 +0,0 @@ -# Explanation - -The explanation guides are written to provide big-picture explanations of how JupyterHub works. They are meant to build your understanding of particular topics. - -```{toctree} -:maxdepth: 2 - -``` diff --git a/docs/source/index-about.md b/docs/source/index-about.md deleted file mode 100644 index 9372c4e7..00000000 --- a/docs/source/index-about.md +++ /dev/null @@ -1,14 +0,0 @@ -# About - -JupyterHub is an open source project and community. It is a part of the -[Jupyter Project](https://jupyter.org). JupyterHub is an open and inclusive -community, and invites contributions from anyone. This section covers information -about our community, as well as ways that you can connect and get involved. - -```{toctree} -:maxdepth: 1 - -contributor-list -changelog -gallery-jhub-deployments -``` diff --git a/docs/source/index-admin.md b/docs/source/index-admin.md deleted file mode 100644 index 05e2bfe1..00000000 --- a/docs/source/index-admin.md +++ /dev/null @@ -1,11 +0,0 @@ -# Administrator's Guide - -This guide covers best-practices, tips, common questions and operations, as -well as other information relevant to running your own JupyterHub over time. - -```{toctree} -:maxdepth: 2 - -admin/capacity-planning -changelog -``` diff --git a/docs/source/index.md b/docs/source/index.md index 83be428e..123da5dc 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -55,8 +55,8 @@ Documentation sections (reorganization in-progress) tutorial/index.md howto/index.md -explanation/index.md reference/index.md +background/index.md faq/index.md ``` @@ -89,14 +89,6 @@ configuration of JupyterHub_. reference/index ``` -### Administrators guide - -```{toctree} -:maxdepth: 2 - -index-admin -``` - ### API Reference ```{toctree} @@ -105,13 +97,6 @@ index-admin api/index ``` -### RBAC Reference - -```{toctree} -:maxdepth: 2 - -rbac/index -``` ### Contributing @@ -128,13 +113,6 @@ help keep our community welcoming to as many people as possible. contributing/index ``` -### About JupyterHub - -```{toctree} -:maxdepth: 2 - -index-about -``` ## Indices and tables diff --git a/docs/source/rbac/index.md b/docs/source/rbac/index.md index 3be61c54..e7030baf 100644 --- a/docs/source/rbac/index.md +++ b/docs/source/rbac/index.md @@ -1,4 +1,4 @@ -(RBAC)= +(rbac)= # JupyterHub RBAC diff --git a/docs/source/rbac/scopes.md b/docs/source/rbac/scopes.md index 175b5fa5..f30c33f9 100644 --- a/docs/source/rbac/scopes.md +++ b/docs/source/rbac/scopes.md @@ -1,8 +1,10 @@ +(jupyterhub-scopes)= + # Scopes in JupyterHub A scope has a syntax-based design that reveals which resources it provides access to. Resources are objects with a type, associated data, relationships to other resources, and a set of methods that operate on them (see [RESTful API](https://restful-api-design.readthedocs.io/en/latest/resources.html) documentation for more information). -`` in the RBAC scope design refers to the resource name in the [JupyterHub's API](../reference/rest-api.md) endpoints in most cases. For instance, `` equal to `users` corresponds to JupyterHub's API endpoints beginning with _/users_. +`` in the RBAC scope design refers to the resource name in the [JupyterHub's API](jupyterhub-rest-API) endpoints in most cases. For instance, `` equal to `users` corresponds to JupyterHub's API endpoints beginning with _/users_. (scope-conventions-target)= @@ -298,6 +300,6 @@ Custom scope _filters_ are NOT supported. ### Scopes and APIs -The scopes are also listed in the [](../reference/rest-api.md) documentation. Each API endpoint has a list of scopes which can be used to access the API; if no scopes are listed, the API is not authenticated and can be accessed without any permissions (i.e., no scopes). +The scopes are also listed in the [](jupyterhub-rest-API) documentation. Each API endpoint has a list of scopes which can be used to access the API; if no scopes are listed, the API is not authenticated and can be accessed without any permissions (i.e., no scopes). Listed scopes by each API endpoint reflect the "lowest" permissions required to gain any access to the corresponding API. For example, posting user's activity (_POST /users/:name/activity_) needs `users:activity` scope. If scope `users` is passed during the request, the access will be granted as the required scope is a subscope of the `users` scope. If, on the other hand, `read:users:activity` scope is passed, the access will be denied. diff --git a/docs/source/rbac/tech-implementation.md b/docs/source/rbac/tech-implementation.md index bc765020..7cc1dc66 100644 --- a/docs/source/rbac/tech-implementation.md +++ b/docs/source/rbac/tech-implementation.md @@ -65,7 +65,7 @@ If the token's scopes are a subset of the token owner's scopes, the token is iss {ref}`Figure 1 ` below illustrates the steps involved. The orange rectangles highlight where in the process the roles and scopes are resolved. -```{figure} ../images/rbac-token-request-chart.png +```{figure} /images/rbac-token-request-chart.png :align: center :name: token-request-chart @@ -91,7 +91,7 @@ The passed scopes are compared to the scopes required to access the API as follo {ref}`Figure 2 ` illustrates this process highlighting the steps where the role and scope resolutions as well as filtering occur in orange. -```{figure} ../images/rbac-api-request-chart.png +```{figure} /images/rbac-api-request-chart.png :align: center :name: api-request-chart diff --git a/docs/source/rbac/upgrade.md b/docs/source/rbac/upgrade.md index c6f85717..22f017bd 100644 --- a/docs/source/rbac/upgrade.md +++ b/docs/source/rbac/upgrade.md @@ -45,7 +45,7 @@ OAuth token is issued by the Hub to a single-user server when the user logs in. API token is issued by the Hub to a single-user server when launched and is used to communicate with the Hub's APIs such as posting activity or completing the OAuth flow. This token has no expiry by default. -API tokens can also be issued to users via API ([_/hub/token_](../reference/urls.md) or [_POST /users/:username/tokens_](../reference/rest-api.md)) and services via `jupyterhub_config.py` to perform API requests. +API tokens can also be issued to users via API ([_/hub/token_](jupyterhub-url) or [_POST /users/:username/tokens_](jupyterhub-rest-API)) and services via `jupyterhub_config.py` to perform API requests. ### With RBAC diff --git a/docs/source/rbac/use-cases.md b/docs/source/rbac/use-cases.md index d5877523..f13e6b75 100644 --- a/docs/source/rbac/use-cases.md +++ b/docs/source/rbac/use-cases.md @@ -3,13 +3,13 @@ To determine which scopes a role should have, one can follow these steps: 1. Determine what actions the role holder should have/have not access to -2. Match the actions against the [JupyterHub's APIs](../reference/rest-api.md) +2. Match the actions against the [JupyterHub's APIs](jupyterhub-rest-API) 3. Check which scopes are required to access the APIs 4. Combine scopes and subscopes if applicable 5. Customize the scopes with filters if needed 6. Define the role with required scopes and assign to users/services/groups/tokens -Below, different use cases are presented on how to use the [RBAC framework](./index.md) +Below, different use cases are presented on how to use the [RBAC framework](rbac) ## Service to cull idle servers diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md index 5a253089..cc14615b 100644 --- a/docs/source/reference/index.md +++ b/docs/source/reference/index.md @@ -10,14 +10,11 @@ what happens under-the-hood when you deploy and configure your JupyterHub. technical-overview urls -websecurity authenticators spawners services rest-api monitoring -database ../events/index config-reference -oauth ``` diff --git a/docs/source/reference/urls.md b/docs/source/reference/urls.md index c2ac95d5..61a92d75 100644 --- a/docs/source/reference/urls.md +++ b/docs/source/reference/urls.md @@ -1,3 +1,5 @@ +(jupyterhub-url)= + # JupyterHub URL scheme This document describes how JupyterHub routes requests. From a084d2310747010ca9fc65ee137d02a4b157c533 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 08:32:24 +0000 Subject: [PATCH 11/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/background/index.md | 5 ++++- docs/source/index.md | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/background/index.md b/docs/source/background/index.md index 3a446aab..ebffe848 100644 --- a/docs/source/background/index.md +++ b/docs/source/background/index.md @@ -3,6 +3,7 @@ _Background_ provides some basic information about JupyterHub and big-picture illustrations of how it works. This section is meant to build your understanding of particular topics and JupyterHub in general. ## About JupyterHub + JupyterHub is an open source project and community. It is a part of the [Jupyter Project](https://jupyter.org). This section covers information about our community as well as institutions that are presently using JupyterHub. @@ -16,6 +17,7 @@ about/contributor-list ``` ## Administration + This section provides information relevant to running your own JupyterHub over time. ```{toctree} @@ -28,10 +30,11 @@ admin/oauth ``` ## JupyterHub RBAC + This section covers how Role Based Access Control (RBAC) is implemented in JupyterHub to control access to Jupyterhub's API resources. ```{toctree} :maxdepth: 2 ../rbac/index -``` \ No newline at end of file +``` diff --git a/docs/source/index.md b/docs/source/index.md index 123da5dc..d0042006 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -97,7 +97,6 @@ reference/index api/index ``` - ### Contributing We welcome you to contribute to JupyterHub in ways that are most exciting @@ -113,7 +112,6 @@ help keep our community welcoming to as many people as possible. contributing/index ``` - ## Indices and tables - {ref}`genindex` From 637cafcf6e798c57909a2d772237670136a7889c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 04:37:54 +0000 Subject: [PATCH 12/18] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v2.0.0 → v2.0.1](https://github.com/PyCQA/autoflake/compare/v2.0.0...v2.0.1) - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9bf9a911..97f0fefc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: # Autoformat: Python code - repo: https://github.com/PyCQA/autoflake - rev: v2.0.0 + rev: v2.0.1 hooks: - id: autoflake # args ref: https://github.com/PyCQA/autoflake#advanced-usage @@ -39,7 +39,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black From be07c7ef31ea586fcb49143e8dc6b942aadbb8ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 04:40:22 +0000 Subject: [PATCH 13/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/service-fastapi/app/security.py | 1 + jupyterhub/_memoize.py | 1 + jupyterhub/app.py | 1 - jupyterhub/auth.py | 1 + jupyterhub/handlers/base.py | 7 ++++++- jupyterhub/scopes.py | 2 +- jupyterhub/services/auth.py | 1 + jupyterhub/singleuser/mixins.py | 1 + jupyterhub/tests/conftest.py | 1 - jupyterhub/tests/selenium/test_browser.py | 1 - jupyterhub/tests/test_app.py | 1 - jupyterhub/tests/test_auth.py | 1 - jupyterhub/tests/test_memoize.py | 2 -- jupyterhub/tests/test_roles.py | 1 - jupyterhub/tests/test_scopes.py | 2 -- jupyterhub/utils.py | 2 +- setup.py | 4 ++-- 17 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/service-fastapi/app/security.py b/examples/service-fastapi/app/security.py index 63fd2a5e..14c66d2b 100644 --- a/examples/service-fastapi/app/security.py +++ b/examples/service-fastapi/app/security.py @@ -32,6 +32,7 @@ if os.environ.get("JUPYTERHUB_OAUTH_SCOPES"): else: access_scopes = ["access:services"] + ### For consideration: optimize performance with a cache instead of ### always hitting the Hub api? async def get_current_user( diff --git a/jupyterhub/_memoize.py b/jupyterhub/_memoize.py index 21907b1d..7266340f 100644 --- a/jupyterhub/_memoize.py +++ b/jupyterhub/_memoize.py @@ -83,6 +83,7 @@ def lru_cache_key(key_func, maxsize=1024): def cache_func(func): cache = LRUCache(maxsize=maxsize) + # the actual decorated function: @wraps(func) def cached(*args, **kwargs): diff --git a/jupyterhub/app.py b/jupyterhub/app.py index bfd436ff..29822429 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2929,7 +2929,6 @@ class JupyterHub(Application): init_spawners_future.add_done_callback(log_init_time) try: - # don't allow a zero timeout because we still need to be sure # that the Spawner objects are defined and pending await gen.with_timeout( diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 7798e0ef..3e2b117d 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -357,6 +357,7 @@ class Authenticator(LoggingConfigurable): ), DeprecationWarning, ) + # use old name instead of new # if old name is overridden in subclass def _new_calls_old(old_name, *args, **kwargs): diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index aa71c9e8..0a32292c 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -664,7 +664,12 @@ class BaseHandler(RequestHandler): next_url = "//" + next_url.lstrip("/") parsed_next_url = urlparse(next_url) - if (next_url + '/').startswith((f'{proto}://{host}/', f'//{host}/',)) or ( + if (next_url + '/').startswith( + ( + f'{proto}://{host}/', + f'//{host}/', + ) + ) or ( self.subdomain_host and parsed_next_url.netloc and ("." + parsed_next_url.netloc).endswith( diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index fb345f3d..5d21d5be 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -685,7 +685,7 @@ def _check_scope_access(api_handler, req_scope, **kwargs): req_scope, ) return True - for (filter_, filter_value) in kwargs.items(): + for filter_, filter_value in kwargs.items(): if filter_ in sub_scope and filter_value in sub_scope[filter_]: app_log.debug("Argument-based access to %s via %s", api_name, req_scope) return True diff --git a/jupyterhub/services/auth.py b/jupyterhub/services/auth.py index 24106ee2..f7a6c87e 100644 --- a/jupyterhub/services/auth.py +++ b/jupyterhub/services/auth.py @@ -1146,6 +1146,7 @@ class HubAuthenticated: except UserNotAllowed as e: # cache None, in case get_user is called again while processing the error self._hub_auth_user_cache = None + # Override redirect so if/when tornado @web.authenticated # tries to redirect to login URL, 403 will be raised instead. # This is not the best, but avoids problems that can be caused diff --git a/jupyterhub/singleuser/mixins.py b/jupyterhub/singleuser/mixins.py index dfad82ca..27868cb1 100755 --- a/jupyterhub/singleuser/mixins.py +++ b/jupyterhub/singleuser/mixins.py @@ -957,6 +957,7 @@ def make_singleuser_app(App): merged_flags = {} merged_flags.update(empty_parent_app.flags or {}) merged_flags.update(flags) + # create mixed-in App class, bringing it all together class SingleUserNotebookApp(SingleUserNotebookAppMixin, App): aliases = merged_aliases diff --git a/jupyterhub/tests/conftest.py b/jupyterhub/tests/conftest.py index d0480362..7d3b9a0e 100644 --- a/jupyterhub/tests/conftest.py +++ b/jupyterhub/tests/conftest.py @@ -290,7 +290,6 @@ async def _mockservice(request, app, external=False, url=False): spec['url'] = 'http://127.0.0.1:%i' % random_port() if external: - spec['oauth_redirect_uri'] = 'http://127.0.0.1:%i' % random_port() event_loop = asyncio.get_running_loop() diff --git a/jupyterhub/tests/selenium/test_browser.py b/jupyterhub/tests/selenium/test_browser.py index d86e8500..e42dcfb7 100644 --- a/jupyterhub/tests/selenium/test_browser.py +++ b/jupyterhub/tests/selenium/test_browser.py @@ -168,7 +168,6 @@ async def test_open_url_login( form_action, user, ): - await open_url(app, browser, path=url) url_new = url_path_join(public_host(app), app.hub.base_url, url_concat(url, params)) await in_thread(browser.get, url_new) diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py index a26d191f..cae40a60 100644 --- a/jupyterhub/tests/test_app.py +++ b/jupyterhub/tests/test_app.py @@ -233,7 +233,6 @@ def test_cookie_secret_string_(): async def test_load_groups(tmpdir, request): - to_load = { 'blue': { 'users': ['cyclops', 'rogue', 'wolverine'], diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index db4fd8bc..8bd88fd8 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -559,7 +559,6 @@ class MockGroupsAuthenticator(auth.Authenticator): async def test_auth_managed_groups( app, user, group, authenticated_groups, refresh_groups ): - authenticator = MockGroupsAuthenticator( parent=app, authenticated_groups=authenticated_groups, diff --git a/jupyterhub/tests/test_memoize.py b/jupyterhub/tests/test_memoize.py index c37942a8..3ae120ff 100644 --- a/jupyterhub/tests/test_memoize.py +++ b/jupyterhub/tests/test_memoize.py @@ -25,7 +25,6 @@ def test_lru_cache(): def test_lru_cache_key(): - call_count = 0 @lru_cache_key(frozenset) @@ -47,7 +46,6 @@ def test_lru_cache_key(): def test_do_not_cache(): - call_count = 0 @lru_cache_key(lambda arg: arg) diff --git a/jupyterhub/tests/test_roles.py b/jupyterhub/tests/test_roles.py index 3198e59a..cec5c0df 100644 --- a/jupyterhub/tests/test_roles.py +++ b/jupyterhub/tests/test_roles.py @@ -1243,7 +1243,6 @@ async def test_admin_role_respects_config(): ], ) async def test_admin_role_membership(in_db, role_users, admin_users, expected_members): - load_roles = [] if role_users is not None: load_roles.append({"name": "admin", "users": role_users}) diff --git a/jupyterhub/tests/test_scopes.py b/jupyterhub/tests/test_scopes.py index 5facb172..8ceef6da 100644 --- a/jupyterhub/tests/test_scopes.py +++ b/jupyterhub/tests/test_scopes.py @@ -971,7 +971,6 @@ def test_intersect_groups(request, db, left, right, expected, groups): async def test_list_users_filter( app, group, create_service_with_scopes, scopes, expected ): - # create users: for i in (1, 2): user = add_user(app.db, app, name=f'in-{i}') @@ -1028,7 +1027,6 @@ async def test_list_users_filter( async def test_list_groups_filter( request, app, create_service_with_scopes, scopes, expected ): - # create groups: groups = [] for i in (1, 2, 3): diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 291c412b..36789698 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -710,7 +710,7 @@ def get_accepted_mimetype(accept_header, choices=None): Return `None` if choices is given and no match is found, or nothing is specified. """ - for (mime, params, q) in _parse_accept_header(accept_header): + for mime, params, q in _parse_accept_header(accept_header): if choices: if mime in choices: return mime diff --git a/setup.py b/setup.py index 5670167e..955669c9 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def get_data_files(): """Get data files in share/jupyter""" data_files = [] - for (d, dirs, filenames) in os.walk(share_jupyterhub): + for d, dirs, filenames in os.walk(share_jupyterhub): rel_d = os.path.relpath(d, here) data_files.append((rel_d, [os.path.join(rel_d, f) for f in filenames])) return data_files @@ -237,7 +237,7 @@ class CSS(BaseCommand): earliest_target = sorted(mtime(t) for t in targets)[0] # check if any .less files are newer than the generated targets - for (dirpath, dirnames, filenames) in os.walk(static): + for dirpath, dirnames, filenames in os.walk(static): for f in filenames: if f.endswith('.less'): path = pjoin(static, dirpath, f) From af1dd54470cd1be0b46c9d593950e421b3e34b26 Mon Sep 17 00:00:00 2001 From: mouse1203 Date: Tue, 7 Feb 2023 08:55:11 +0100 Subject: [PATCH 14/18] selenium:updating cases are related to Admin UI According to the review updated cases covered the Admin UI: - rename the function, - add docstring, - add parametrization, - use the fixture admin_user instead of the function --- jupyterhub/tests/selenium/test_browser.py | 186 +++++++++++++--------- 1 file changed, 115 insertions(+), 71 deletions(-) diff --git a/jupyterhub/tests/selenium/test_browser.py b/jupyterhub/tests/selenium/test_browser.py index a25e669d..1b241c39 100644 --- a/jupyterhub/tests/selenium/test_browser.py +++ b/jupyterhub/tests/selenium/test_browser.py @@ -997,7 +997,8 @@ async def test_oauth_page( async def open_admin_page(app, browser, user): - """function to open the admin page""" + """Login as `user` and open the admin page""" + admin_page = url_escape(app.base_url) + "hub/admin" await open_url(app, browser, path="/login?next=" + admin_page) await login(browser, user.name, pass_w=str(user.name)) @@ -1010,44 +1011,41 @@ async def open_admin_page(app, browser, user): ) -def create_admin_user(create_user_with_scopes): - # create user with appropriate access permissions - user_scopes = [ - "admin-ui", - "admin:users", - "admin:servers", - "admin:groups", - "access:servers", - "list:users", - ] - admin_user = create_user_with_scopes(*user_scopes) - return admin_user - - def create_list_of_users(create_user_with_scopes, n): return [create_user_with_scopes(["users"]) for i in range(1, n)] -async def test_open_admin_page(app, browser, create_user_with_scopes): - admin_user = create_admin_user(create_user_with_scopes) +async def test_open_admin_page(app, browser, admin_user): + await open_admin_page(app, browser, admin_user) assert '/hub/admin' in browser.current_url -def check_btns(browser, name): +def get_users_buttons(browser, class_name): + """returns the list of buttons in the user row(s) that match this class name""" + all_btns = browser.find_elements( - By.XPATH, '//*[@data-testid="user-row-server-activity"]//button' + By.XPATH, + f'//*[@data-testid="user-row-server-activity"]//button[contains(@class,"{class_name}")]', ) - btns = list(filter(lambda x: name in x.get_attribute('class'), all_btns)) - return btns + return all_btns -async def test_start_stop_all_servers_on_admin_page( - app, browser, create_user_with_scopes -): +async def click_and_wait_paging_btn(browser, buttons_number): + """interecrion with paging buttons, where number 1 = previous and number 2 = next""" + # number 1 - previous button, number 2 - next button + await click( + browser, + ( + By.XPATH, + f'//*[@class="pagination-footer"]//button[contains(@class, "btn-light")][{buttons_number}]', + ), + ) - user_admin = create_admin_user(create_user_with_scopes) - await open_admin_page(app, browser, user_admin) + +async def test_start_stop_all_servers_on_admin_page(app, browser, admin_user): + + await open_admin_page(app, browser, admin_user) # get total count of users from db users_count_db = app.db.query(orm.User).count() @@ -1059,7 +1057,7 @@ async def test_start_stop_all_servers_on_admin_page( ) # verify Start All and Stop All buttons are displayed - assert start_all_btn.is_displayed(), stop_all_btn.is_displayed() + assert start_all_btn.is_displayed() and stop_all_btn.is_displayed() async def click_all_btns(browser, btn_type, btn_await): await click( @@ -1084,7 +1082,10 @@ async def test_start_stop_all_servers_on_admin_page( # no access buttons are displayed class_names = ["stop-button", "primary", "start-button", "secondary"] - btns = {name: check_btns(browser, name) for name in class_names} + btns = { + class_name: get_users_buttons(browser, class_name) for class_name in class_names + } + print(btns) assert ( len(btns["start-button"]) == len(btns["secondary"]) @@ -1096,12 +1097,14 @@ async def test_start_stop_all_servers_on_admin_page( # start all servers via the Start All await click_all_btns(browser, "start-all", "stop-button") # Start All and Stop All are still displayed - assert start_all_btn.is_displayed(), stop_all_btn.is_displayed() + assert start_all_btn.is_displayed() and stop_all_btn.is_displayed() # users´numbers are the same as numbers of the stop button and the Access button # no Start server buttons are displayed # no Spawn page buttons are displayed - btns = {name: check_btns(browser, name) for name in class_names} + btns = { + class_name: get_users_buttons(browser, class_name) for class_name in class_names + } assert ( len(btns["stop-button"]) == len(btns["primary"]) == len(users) == users_count_db ) @@ -1114,8 +1117,10 @@ async def test_start_stop_all_servers_on_admin_page( # users´numbers are the same as numbers of the start button and the Spawn page button # no Stop server buttons are displayed # no access buttons are displayed - assert start_all_btn.is_displayed(), stop_all_btn.is_displayed() - btns = {name: check_btns(browser, name) for name in class_names} + assert start_all_btn.is_displayed() and stop_all_btn.is_displayed() + btns = { + class_name: get_users_buttons(browser, class_name) for class_name in class_names + } assert ( len(btns["start-button"]) == len(btns["secondary"]) @@ -1125,9 +1130,12 @@ async def test_start_stop_all_servers_on_admin_page( assert not btns["stop-button"] and not btns["primary"] -async def test_paging_on_admin_page(app, browser, create_user_with_scopes, n=60): - admin_user = create_admin_user(create_user_with_scopes) - create_list_of_users(create_user_with_scopes, n) +@pytest.mark.parametrize("added_count_users", [10, 47, 48, 49, 110]) +async def test_paging_on_admin_page( + app, browser, admin_user, added_count_users, create_user_with_scopes +): + + create_list_of_users(create_user_with_scopes, added_count_users) await open_admin_page(app, browser, admin_user) users = browser.find_elements(By.XPATH, '//td[@data-testid="user-row-name"]') @@ -1138,43 +1146,53 @@ async def test_paging_on_admin_page(app, browser, create_user_with_scopes, n=60) displaying = browser.find_element( By.XPATH, '//*[@class="pagination-footer"]//*[contains(text(),"Displaying")]' ) - btn_previous = browser.find_element( By.XPATH, '//*[@class="pagination-footer"]//span[contains(text(),"Previous")]' ) btn_next = browser.find_element( By.XPATH, '//*[@class="pagination-footer"]//span[contains(text(),"Next")]' ) - - async def click_and_wait(browser, buttons_number): - # number 1 - previous button, number 2 - next button - await click( - browser, - ( - By.XPATH, - f'//*[@class="pagination-footer"]//button[contains(@class, "btn-light")][{buttons_number}]', - ), - ) - assert f"0-{min(users_count_db,50)}" in displaying.text if users_count_db > 50: assert btn_next.get_dom_attribute("class") == "active-pagination" # click on Next button - await click_and_wait(browser, buttons_number=2) - assert f"50-{users_count_db}" in displaying.text + await click_and_wait_paging_btn(browser, buttons_number=2) + if users_count_db <= 100: + assert f"50-{users_count_db}" in displaying.text + else: + assert "50-100" in displaying.text + assert btn_next.get_dom_attribute("class") == "active-pagination" assert btn_previous.get_dom_attribute("class") == "active-pagination" # click on Previous button - await click_and_wait(browser, buttons_number=1) + await click_and_wait_paging_btn(browser, buttons_number=1) else: assert btn_previous.get_dom_attribute("class") == "inactive-pagination" assert btn_next.get_dom_attribute("class") == "inactive-pagination" +@pytest.mark.parametrize( + "added_count_users, search_value", + [ + # the value of search is absent =>the expected result null records are found + (10, "not exists"), + # a search value is a middle part of users name (number,symbol,letter) + (25, "r_5"), + # a search value equals to number + (50, "1"), + # searching result shows on more than one page + (60, "user"), + ], +) async def test_search_on_admin_page( - app, browser, create_user_with_scopes, search_value="user", n=60 + app, + browser, + admin_user, + create_user_with_scopes, + added_count_users, + search_value, ): - admin_user = create_admin_user(create_user_with_scopes) - create_list_of_users(create_user_with_scopes, n) + + create_list_of_users(create_user_with_scopes, added_count_users) await open_admin_page(app, browser, admin_user) element_search = browser.find_element(By.XPATH, '//input[@name="user_search"]') element_search.send_keys(search_value) @@ -1183,29 +1201,47 @@ async def test_search_on_admin_page( users_count_db_filtered = ( app.db.query(orm.User).filter(orm.User.name.like(f'%{search_value}%')).count() ) - - filtered_list = browser.find_elements(By.XPATH, '//*[@class="user-row"]') + filtered_list_on_page = browser.find_elements(By.XPATH, '//*[@class="user-row"]') # check that count of users matches with number of users on the footer displaying = browser.find_element( By.XPATH, '//*[@class="pagination-footer"]//*[contains(text(),"Displaying")]' ) - if users_count_db_filtered <= 50: - assert "0-" + str(users_count_db_filtered) in displaying.text - assert len(filtered_list) == users_count_db_filtered - else: - assert "0-50" in displaying.text - assert len(filtered_list) == 50 # check that users names contain the search value in the filtered list - for element in filtered_list: + for element in filtered_list_on_page: name = element.find_element( By.XPATH, '//*[@data-testid="user-row-name"]//span[contains(@data-testid, "user-name-div")]', ) assert search_value in name.text + if users_count_db_filtered <= 50: + assert "0-" + str(users_count_db_filtered) in displaying.text + assert len(filtered_list_on_page) == users_count_db_filtered + else: + assert "0-50" in displaying.text + assert len(filtered_list_on_page) == 50 + # click on Next button to verify that the rest part of filtered list is displayed on the next page + await click_and_wait_paging_btn(browser, buttons_number=2) + filtered_list_on_next_page = browser.find_elements( + By.XPATH, '//*[@class="user-row"]' + ) + assert users_count_db_filtered - 50 == len(filtered_list_on_next_page) + for element in filtered_list_on_next_page: + name = element.find_element( + By.XPATH, + '//*[@data-testid="user-row-name"]//span[contains(@data-testid, "user-name-div")]', + ) + assert search_value in name.text +@pytest.mark.parametrize("added_count_users,index_user_1, index_user_2", [(5, 1, 0)]) async def test_start_stop_server_on_admin_page( - app, browser, create_user_with_scopes, n=5 + app, + browser, + admin_user, + create_user_with_scopes, + added_count_users, + index_user_1, + index_user_2, ): async def start_user(browser, expected_user): start_button_xpath = f'//a[contains(@href, "spawn/{expected_user[0]}")]/preceding-sibling::button[contains(@class, "start-button")]' @@ -1241,12 +1277,12 @@ async def test_start_stop_server_on_admin_page( await click(browser, (By.XPATH, stop_btn_xpath)) await webdriver_wait(browser, EC.staleness_of(stop_btn)) - user_admin = create_admin_user(create_user_with_scopes) - create_list_of_users(create_user_with_scopes, n) - await open_admin_page(app, browser, user_admin) + create_list_of_users(create_user_with_scopes, added_count_users) + await open_admin_page(app, browser, admin_user) users = browser.find_elements(By.XPATH, '//td[@data-testid="user-row-name"]') users_list = [user.text for user in users] - expected_user = [users_list[n - 3], users_list[n - 4]] + + expected_user = [users_list[index_user_1], users_list[index_user_2]] spawn_page_btns = browser.find_elements( By.XPATH, '//*[@data-testid="user-row-server-activity"]//a[contains(@href, "spawn/")]', @@ -1261,7 +1297,9 @@ async def test_start_stop_server_on_admin_page( # click on Start button await start_user(browser, expected_user) class_names = ["stop-button", "primary", "start-button", "secondary"] - btns = {name: check_btns(browser, name) for name in class_names} + btns = { + class_name: get_users_buttons(browser, class_name) for class_name in class_names + } assert len(btns["stop-button"]) == 1 # click on Spawn page button @@ -1273,18 +1311,24 @@ async def test_start_stop_server_on_admin_page( # wait for javascript to finish loading await wait_for_ready(browser) assert "/hub/admin" in browser.current_url - btns = {name: check_btns(browser, name) for name in class_names} + btns = { + class_name: get_users_buttons(browser, class_name) for class_name in class_names + } assert len(btns["stop-button"]) == len(btns["primary"]) == 2 # click on the Access button await access_srv_user(browser, expected_user) assert "/hub/admin" in browser.current_url - btns = {name: check_btns(browser, name) for name in class_names} + btns = { + class_name: get_users_buttons(browser, class_name) for class_name in class_names + } assert len(btns["stop-button"]) == 2 # click on Stop button for both users await stop_srv_users(browser, expected_user) - btns = {name: check_btns(browser, name) for name in class_names} + btns = { + class_name: get_users_buttons(browser, class_name) for class_name in class_names + } assert len(btns["stop-button"]) == 0 assert len(btns["primary"]) == 0 From c766f5866eb875d6b27a0e20195f14fec14d79a2 Mon Sep 17 00:00:00 2001 From: alwasega Date: Tue, 7 Feb 2023 15:58:05 +0300 Subject: [PATCH 15/18] incorporated changes after review --- docs/source/background/index.md | 40 ------------------- .../about => }/contributor-list.md | 0 .../admin/capacity-planning.md | 0 .../admin/database.md | 0 .../admin/oauth.md | 0 .../admin/websecurity.md | 0 docs/source/explanation/index.md | 26 ++++++++++++ .../about => }/gallery-jhub-deployments.md | 0 docs/source/index-about.md | 14 +++++++ docs/source/index.md | 10 ++++- 10 files changed, 49 insertions(+), 41 deletions(-) delete mode 100644 docs/source/background/index.md rename docs/source/{background/about => }/contributor-list.md (100%) rename docs/source/{background => explanation}/admin/capacity-planning.md (100%) rename docs/source/{background => explanation}/admin/database.md (100%) rename docs/source/{background => explanation}/admin/oauth.md (100%) rename docs/source/{background => explanation}/admin/websecurity.md (100%) create mode 100644 docs/source/explanation/index.md rename docs/source/{background/about => }/gallery-jhub-deployments.md (100%) create mode 100644 docs/source/index-about.md diff --git a/docs/source/background/index.md b/docs/source/background/index.md deleted file mode 100644 index ebffe848..00000000 --- a/docs/source/background/index.md +++ /dev/null @@ -1,40 +0,0 @@ -# Background - -_Background_ provides some basic information about JupyterHub and big-picture illustrations of how it works. This section is meant to build your understanding of particular topics and JupyterHub in general. - -## About JupyterHub - -JupyterHub is an open source project and community. It is a part of the -[Jupyter Project](https://jupyter.org). This section covers information -about our community as well as institutions that are presently using JupyterHub. - -```{toctree} -:maxdepth: 1 - -about/gallery-jhub-deployments -../changelog -about/contributor-list -``` - -## Administration - -This section provides information relevant to running your own JupyterHub over time. - -```{toctree} -:maxdepth: 1 - -admin/capacity-planning -admin/database -admin/websecurity -admin/oauth -``` - -## JupyterHub RBAC - -This section covers how Role Based Access Control (RBAC) is implemented in JupyterHub to control access to Jupyterhub's API resources. - -```{toctree} -:maxdepth: 2 - -../rbac/index -``` diff --git a/docs/source/background/about/contributor-list.md b/docs/source/contributor-list.md similarity index 100% rename from docs/source/background/about/contributor-list.md rename to docs/source/contributor-list.md diff --git a/docs/source/background/admin/capacity-planning.md b/docs/source/explanation/admin/capacity-planning.md similarity index 100% rename from docs/source/background/admin/capacity-planning.md rename to docs/source/explanation/admin/capacity-planning.md diff --git a/docs/source/background/admin/database.md b/docs/source/explanation/admin/database.md similarity index 100% rename from docs/source/background/admin/database.md rename to docs/source/explanation/admin/database.md diff --git a/docs/source/background/admin/oauth.md b/docs/source/explanation/admin/oauth.md similarity index 100% rename from docs/source/background/admin/oauth.md rename to docs/source/explanation/admin/oauth.md diff --git a/docs/source/background/admin/websecurity.md b/docs/source/explanation/admin/websecurity.md similarity index 100% rename from docs/source/background/admin/websecurity.md rename to docs/source/explanation/admin/websecurity.md diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md new file mode 100644 index 00000000..50e6c9f2 --- /dev/null +++ b/docs/source/explanation/index.md @@ -0,0 +1,26 @@ +# Explanation + +_Explanation_ documentation provide big-picture descriptions of how JupyterHub works. This section is meant to build your understanding of particular topics. + +## Administration + +This section provides information relevant to running your own JupyterHub over time. + +```{toctree} +:maxdepth: 1 + +admin/capacity-planning +admin/database +admin/websecurity +admin/oauth +``` + +## JupyterHub RBAC + +This section covers how Role Based Access Control (RBAC) is implemented in JupyterHub to control access to Jupyterhub's API resources. + +```{toctree} +:maxdepth: 2 + +../rbac/index +``` diff --git a/docs/source/background/about/gallery-jhub-deployments.md b/docs/source/gallery-jhub-deployments.md similarity index 100% rename from docs/source/background/about/gallery-jhub-deployments.md rename to docs/source/gallery-jhub-deployments.md diff --git a/docs/source/index-about.md b/docs/source/index-about.md new file mode 100644 index 00000000..3843cd64 --- /dev/null +++ b/docs/source/index-about.md @@ -0,0 +1,14 @@ +# About + +JupyterHub is an open source project and community. It is a part of the +[Jupyter Project](https://jupyter.org). JupyterHub is an open and inclusive +community, and invites contributions from anyone. This section covers information +about our community, as well as ways that you can connect and get involved. + +```{toctree} +:maxdepth: 1 + +contributor-list +changelog +gallery-jhub-deployments +``` \ No newline at end of file diff --git a/docs/source/index.md b/docs/source/index.md index d0042006..2dd53cef 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -56,7 +56,7 @@ Documentation sections (reorganization in-progress) tutorial/index.md howto/index.md reference/index.md -background/index.md +explanation/index.md faq/index.md ``` @@ -112,6 +112,14 @@ help keep our community welcoming to as many people as possible. contributing/index ``` +### About JupyterHub + +```{toctree} +:maxdepth: 2 + +index-about +``` + ## Indices and tables - {ref}`genindex` From ec9e9c3b041a9f764a704c4b78561a2b3c4848a0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:56:24 +0000 Subject: [PATCH 16/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/index-about.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index-about.md b/docs/source/index-about.md index 3843cd64..9372c4e7 100644 --- a/docs/source/index-about.md +++ b/docs/source/index-about.md @@ -11,4 +11,4 @@ about our community, as well as ways that you can connect and get involved. contributor-list changelog gallery-jhub-deployments -``` \ No newline at end of file +``` From 3abce3581cedb77f06959c416754c9a645063695 Mon Sep 17 00:00:00 2001 From: alwasega Date: Wed, 8 Feb 2023 11:24:46 +0300 Subject: [PATCH 17/18] Added signpost comments in explanation/index and rbac/index files --- docs/source/explanation/index.md | 4 ++++ docs/source/rbac/index.md | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md index 50e6c9f2..b8e634c2 100644 --- a/docs/source/explanation/index.md +++ b/docs/source/explanation/index.md @@ -19,6 +19,10 @@ admin/oauth This section covers how Role Based Access Control (RBAC) is implemented in JupyterHub to control access to Jupyterhub's API resources. + + ```{toctree} :maxdepth: 2 diff --git a/docs/source/rbac/index.md b/docs/source/rbac/index.md index e7030baf..18eaddad 100644 --- a/docs/source/rbac/index.md +++ b/docs/source/rbac/index.md @@ -1,3 +1,8 @@ + + (rbac)= # JupyterHub RBAC From defde6774630dd998c75bef5de5157731e1c4243 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Feb 2023 08:22:42 +0000 Subject: [PATCH 18/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/rbac/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/rbac/index.md b/docs/source/rbac/index.md index 18eaddad..af047fa3 100644 --- a/docs/source/rbac/index.md +++ b/docs/source/rbac/index.md @@ -1,6 +1,6 @@ (rbac)=