From 14334116fc0fa7da4e14c05dd9ba7c173d212d77 Mon Sep 17 00:00:00 2001 From: Mark Dawson Date: Fri, 22 Sep 2023 11:37:29 +0100 Subject: [PATCH] check global config for hub url and (#507) gui: open the hub url if configured --- changes.d/507.feat.md | 1 + cylc/uiserver/scripts/gui.py | 37 ++++++++++++++++++++++-- cylc/uiserver/tests/conftest.py | 51 +++++++++++++++++++++++++++++++++ cylc/uiserver/tests/test_gui.py | 26 ++++++++++++++--- 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 changes.d/507.feat.md diff --git a/changes.d/507.feat.md b/changes.d/507.feat.md new file mode 100644 index 00000000..f6f4c831 --- /dev/null +++ b/changes.d/507.feat.md @@ -0,0 +1 @@ +Added functionality for routing to a multiuser deployment when running cylc gui command. \ No newline at end of file diff --git a/cylc/uiserver/scripts/gui.py b/cylc/uiserver/scripts/gui.py index 71514768..6aa86ca1 100644 --- a/cylc/uiserver/scripts/gui.py +++ b/cylc/uiserver/scripts/gui.py @@ -30,8 +30,10 @@ from requests.exceptions import RequestException import requests import sys +from textwrap import dedent from typing import Optional import webbrowser +from getpass import getuser from cylc.flow.id_cli import parse_id_async @@ -40,6 +42,8 @@ WorkflowFilesError ) +from cylc.flow.cfgspec.glbl_cfg import glbl_cfg + from cylc.uiserver import init_log from cylc.uiserver.app import ( CylcUIServer, @@ -51,8 +55,32 @@ def main(*argv): init_log() + hub_url = glbl_cfg().get(['hub', 'url']) jp_server_opts, new_gui, workflow_id = parse_args_opts() - if '--help' not in sys.argv: + if '--help' in sys.argv and hub_url: + print( + dedent(''' + cylc gui [WORKFLOW] + + Open the Cylc GUI in a new web browser tab. + + If WORKFLOW is specified, the GUI will open on this workflow. + + This command has been configured to use a centrally configured + Jupyter Hub instance rather than start a standalone server. + To see the configuration options for the server run + "cylc gui --help-all", these options can be configured in the + Jupyter configuration files using "c.Spawner.cmd", see the Cylc + and Jupyter Hub documentation for more details. + ''')) + return + if not {'--help', '--help-all'} & set(sys.argv): + if hub_url: + print(f"Running on {hub_url } as specified in global config.") + webbrowser.open( + update_url(hub_url, workflow_id), autoraise=True + ) + return # get existing jpserver--open.html files # check if the server is available for use # prompt for user whether to clean files for un-usable uiservers @@ -190,6 +218,7 @@ def get_arg_parser(): def update_url(url, workflow_id): """ Update the url to open at the correct workflow in the gui. """ + hub_url = glbl_cfg().get(['hub', 'url']) if not url: return split_url = url.split('/workspace/') @@ -212,4 +241,8 @@ def update_url(url, workflow_id): return url.replace(old_workflow, workflow_id) else: # current url points to dashboard, update to point to workflow - return f"{url}/workspace/{workflow_id}" + if hub_url: + return (f"{url}/user/{getuser()}/{CylcUIServer.name}" + f"/#/workspace/{workflow_id}") + else: + return f"{url}/workspace/{workflow_id}" diff --git a/cylc/uiserver/tests/conftest.py b/cylc/uiserver/tests/conftest.py index bb1d512a..61d31b8d 100644 --- a/cylc/uiserver/tests/conftest.py +++ b/cylc/uiserver/tests/conftest.py @@ -43,6 +43,9 @@ from cylc.uiserver.data_store_mgr import DataStoreMgr from cylc.uiserver.workflows_mgr import WorkflowsManager +from cylc.flow.cfgspec.globalcfg import SPEC +from cylc.flow.parsec.config import ParsecConfig +from cylc.flow.parsec.validate import cylc_config_validate class AsyncClientFixture(WorkflowRuntimeClient): pattern = zmq.REQ @@ -360,3 +363,51 @@ def workflow_run_dir(request): yield flow_name, log_dir if not request.session.testsfailed: rmtree(run_dir) + +@pytest.fixture +def mock_glbl_cfg(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """A Pytest fixture for fiddling global config values. + + * Hacks the specified `glbl_cfg` object. + * Can be called multiple times within a test function. + + Args: + pypath (str): + The python-like path to the global configuation object you want + to fiddle. + E.G. if you want to hack the `glbl_cfg` in + `cylc.flow.scheduler` you would provide + `cylc.flow.scheduler.glbl_cfg` + global_config (str): + The globlal configuration as a multi-line string. + + Example: + Change the value of `UTC mode` in the global config as seen from + `the scheduler` module. + + def test_something(mock_glbl_cfg): + mock_glbl_cfg( + 'cylc.flow.scheduler.glbl_cfg', + ''' + [scheduler] + UTC mode = True + ''' + ) + + """ + # TODO: modify Parsec so we can use StringIO rather than a temp file. + def _mock_glbl_cfg(pypath: str, global_config: str) -> None: + nonlocal tmp_path, monkeypatch + global_config_path = tmp_path / 'global.cylc' + global_config_path.write_text(global_config) + glbl_cfg = ParsecConfig(SPEC, validator=cylc_config_validate) + glbl_cfg.loadcfg(global_config_path) + + def _inner(cached=False): + nonlocal glbl_cfg + return glbl_cfg + + monkeypatch.setattr(pypath, _inner) + + yield _mock_glbl_cfg + rmtree(tmp_path) diff --git a/cylc/uiserver/tests/test_gui.py b/cylc/uiserver/tests/test_gui.py index 3628c699..8b135e63 100644 --- a/cylc/uiserver/tests/test_gui.py +++ b/cylc/uiserver/tests/test_gui.py @@ -17,10 +17,10 @@ from glob import glob import os from pathlib import Path +from getpass import getuser import pytest from random import randint import requests -from shutil import rmtree from time import sleep from cylc.uiserver.scripts.gui import ( @@ -30,46 +30,64 @@ ) @pytest.mark.parametrize( - 'existing_content,workflow_id,expected_updated_content', + 'existing_content,workflow_id,expected_updated_content,hub_url', [ pytest.param( 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#', None, 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#', + '', id='existing_no_workflow_new_no_workflow' ), pytest.param( 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#', 'some/workflow', 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#/workspace/some/workflow', + '', id='existing_no_workflow_new_workflow' ), + pytest.param( + 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#', + 'some/hub/workflow', + f'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#/user/{getuser()}/cylc/#/workspace/some/hub/workflow', + 'localhost:8000', + id='existing_no_workflow_new_workflow_hub' + ), pytest.param( 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#/workspace/some/workflow', 'another/flow', 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#/workspace/another/flow', + '', id='existing_workflow_new_workflow' ), pytest.param( 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#/workspace/some/workflow', None, 'http://localhost:8892/cylc/?token=1234567890some_big_long_token1234567890#', + '', id='existing_workflow_no_new_workflow' ), pytest.param( '', 'another/flow', None, + '', id='no_url_no_change' ), ] ) + def test_update_html_file_updates_gui_file( existing_content, workflow_id, - expected_updated_content): + expected_updated_content, + hub_url, + mock_glbl_cfg): """Tests url is updated correctly""" - + mock_glbl_cfg('cylc.uiserver.scripts.gui.glbl_cfg', + f'''[hub] + url = {hub_url} + ''') updated_file_content = update_url(existing_content, workflow_id) assert updated_file_content == expected_updated_content