diff --git a/docs/playbooks/rstudio_server.md b/docs/playbooks/rstudio_server.md new file mode 100644 index 00000000..2f217897 --- /dev/null +++ b/docs/playbooks/rstudio_server.md @@ -0,0 +1,33 @@ +# Playbook RStudio Server +[back to index](../index.md#Roles) + +## Summary + +This role installs [RStudio Server](https://posit.co/products/open-source/rstudio/), an open-source IDE for R programming. It integrates RStudio Server within a JupyterHub environment, enabling users to access the IDE via their browsers. Login via SRAM. + +The playbook supports two modes of operation: +1. **Standalone mode**: RStudio Server runs independently of JupyterLab. Users are immediately directed to RStudio. +2. **Non-standalone mode**: RStudio Server is configured as a JupyterLab extension and can be launched by users within the JupyterLab interface, which they see when they login to the wroskpace. This mode integrates RStudio seamlessly into a multi-application JupyterHub setup. + +### Security note + +In both modes, RStudio Server instances are spawned by JupyterHub and run under the user’s credentials. Each user has an isolated instance, ensuring data separation. RStudio is run using a Unix Domain Socket, ensuring that users with shell access cannot directly connect to another user's server. + +## Variables + +- `rstudio_fqdn`: String. Fully Qualified Domain Name (FQDN) where RStudio Server/JupyterHub will be served. Defaults to `localhost`. +- `rstudio_base_path`: String. URI at which RStudio Server is served. Default: `/`. +- `rstudio_jupyter_venv_path`: String. Location of the virtual environment for JupyterHub and related components. Default: `/usr/local/jupyterhub`. +- `rstudio_standalone`: Boolean. Specifies whether RStudio Server should be installed in standalone mode. Default: `false`. +- `rstudio_pip_cmd`: String. Command used to install pip packages (default: `uv`, can be set to `pip`). + +## See also + +- Role [rstudio](../roles/rstudio.md) +- Role [jupyterhub_standalone_proxy](../roles/jupyterhub_standalone_proxy.md) +- Role [jupyterhub_app](../roles/jupyterhub_app.md) + +## History +2026 Written by Dawa Ometto (Utrecht University). + +[back to index](../index.md#Roles) \ No newline at end of file diff --git a/docs/roles/jupyterhub_app.md b/docs/roles/jupyterhub_app.md index 50318512..745a876c 100644 --- a/docs/roles/jupyterhub_app.md +++ b/docs/roles/jupyterhub_app.md @@ -106,8 +106,8 @@ The resulting config for a JupyterLab server extension will look as follows: settings = { "Example": {'command': ['/usr/bin/python3', '-m' 'http.server', '-p', '{port}'], 'launcher_entry': {'icon_path': '/path/to/my/icon.svg', 'category': 'Other'} } -c.ServerProxy.servers.update({ -}) # use update instead of assignment so multiple apps can be added to the config. +c.ServerProxy.servers.update(settings +) # use update instead of assignment so multiple apps can be added to the config. ``` The resulting config for a standalone app will look as follows: diff --git a/docs/roles/rstudio.md b/docs/roles/rstudio.md index b9e3326f..545eb992 100644 --- a/docs/roles/rstudio.md +++ b/docs/roles/rstudio.md @@ -2,16 +2,22 @@ [back to index](../index.md#Roles) ## Summary -Installs R-studio on Ubuntu 20.04 and 22.04 +Installs RStudio Desktop or Server. ## Requires -Ubuntu 20.04 or 22.04 and an installation of R (e.g. by executing the r-workbench playbook first) +- Ubuntu >= 20.04 +- Untested supported for RHEL-like Linux ## Description Installation is done using apt and the debian package available [here](https://posit.co/download/rstudio-desktop/). +## Variables + +- `rstudio_kind`: String. Whether to install the desktop or server version of RStudio. Default: `desktop` (set to `server` alternatively). + ## History 2022 Written by Sytse Groenwold (Universiteit Utrecht) 2024 Updated by Jelle Treep (Utrecht University) +2025 Updated by Dawa Ometto (Utrecht University) [back to index](../index.md#Roles) diff --git a/molecule/playbook-openrefine-standalone/molecule.yml b/molecule/playbook-openrefine-standalone/molecule.yml index 30f490db..1c87164d 100644 --- a/molecule/playbook-openrefine-standalone/molecule.yml +++ b/molecule/playbook-openrefine-standalone/molecule.yml @@ -23,6 +23,7 @@ provisioner: openrefine_max_memory: '60M' jupyterhub_app_pip_executable: uv_pip jupyterhub_auth: noauth + jupyterhub_debug: true jupyterhub_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. jupyterhub_proxy_config: proxy_set_header: diff --git a/molecule/playbook-openrefine/molecule.yml b/molecule/playbook-openrefine/molecule.yml index 59d732f6..ced25bbd 100644 --- a/molecule/playbook-openrefine/molecule.yml +++ b/molecule/playbook-openrefine/molecule.yml @@ -18,6 +18,7 @@ provisioner: - name: jupyterhub path: jupyterhub.yml parameters: + jupyterhub_debug: true jupyter_auth: noauth jupyter_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. jupyter_proxy_config: "{ proxy_set_header: { REMOTE_USER: '$$http_user'} }" diff --git a/molecule/playbook-openvscodeserver-standalone/molecule.yml b/molecule/playbook-openvscodeserver-standalone/molecule.yml new file mode 100644 index 00000000..45a82fb4 --- /dev/null +++ b/molecule/playbook-openvscodeserver-standalone/molecule.yml @@ -0,0 +1,30 @@ +--- +platforms: + - name: workspace-src-ubuntu_jammy-nginx + image: ghcr.io/utrechtuniversity/src-test-workspace:ubuntu_jammy-nginx + command: /sbin/init + pre_build_image: true + published_ports: + - 8080:80 + registry: + url: $DOCKER_REGISTRY + credentials: + username: $DOCKER_USER + password: $DOCKER_PW +provisioner: + name: ansible + env: + components: + - name: openvscodeserver + path: openvscodeserver.yml + parameters: + openvscode_base_path: "/ide" + openvscode_jupyter_force_install: 'true' + jhsp_external_addr: localhost:8080 + jupyterhub_app_pip_executable: uv_pip + jupyterhub_debug: true + jupyterhub_auth: noauth + jupyterhub_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. + jupyterhub_proxy_config: + proxy_set_header: + REMOTE_USER: "$$http_user" # allows us to set the 'user' header in tests to mock login. We need double dollar signs because of the way ansible's `to_yaml` filters escapes! diff --git a/molecule/playbook-openvscodeserver-standalone/verify.yml b/molecule/playbook-openvscodeserver-standalone/verify.yml new file mode 100644 index 00000000..21b30555 --- /dev/null +++ b/molecule/playbook-openvscodeserver-standalone/verify.yml @@ -0,0 +1,22 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Test OpenVSCode Server + ansible.builtin.uri: + url: http://localhost/ide + method: GET + headers: + user: testuser + return_content: true + follow_redirects: true + register: openvscode_login + failed_when: openvscode_login.failed or openvscode_login.url != 'http://localhost/ide/user/testuser/' + retries: 5 + delay: 3 + + - name: Assert content correct + ansible.builtin.assert: + that: + - '"vscode-workbench" in openvscode_login.content' diff --git a/molecule/playbook-openvscodeserver/molecule.yml b/molecule/playbook-openvscodeserver/molecule.yml index ab850e24..8978054c 100644 --- a/molecule/playbook-openvscodeserver/molecule.yml +++ b/molecule/playbook-openvscodeserver/molecule.yml @@ -15,15 +15,15 @@ provisioner: name: ansible env: components: + - name: jupyterhub + path: jupyterhub.yml + parameters: + jupyterhub_debug: true + jupyter_auth: noauth + jupyter_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. + jupyter_proxy_config: "{ proxy_set_header: { REMOTE_USER: '$$http_user'} }" - name: openvscodeserver path: openvscodeserver.yml parameters: openvscode_base_path: "/ide" - openvscode_jupyter_force_install: 'true' - jhsp_external_addr: localhost:8080 - jupyterhub_app_pip_executable: uv_pip - jupyterhub_auth: noauth - jupyterhub_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. - jupyterhub_proxy_config: - proxy_set_header: - REMOTE_USER: "$$http_user" # allows us to set the 'user' header in tests to mock login. We need double dollar signs because of the way ansible's `to_yaml` filters escapes! + openvscode_jupyter_force_install: 'false' diff --git a/molecule/playbook-openvscodeserver/verify.yml b/molecule/playbook-openvscodeserver/verify.yml index 21b30555..e52ab89b 100644 --- a/molecule/playbook-openvscodeserver/verify.yml +++ b/molecule/playbook-openvscodeserver/verify.yml @@ -3,16 +3,16 @@ hosts: all gather_facts: false tasks: - - name: Test OpenVSCode Server + - name: Test Open VSCode ansible.builtin.uri: - url: http://localhost/ide + url: http://localhost/hub/spawn/testuser?next=%2Fhub%2Fuser%2Ftestuser%2Fide%2F method: GET headers: - user: testuser + user: testuser # mock SRAM login return_content: true follow_redirects: true register: openvscode_login - failed_when: openvscode_login.failed or openvscode_login.url != 'http://localhost/ide/user/testuser/' + failed_when: openvscode_login.failed or not openvscode_login.url is match('http://localhost/user/testuser/ide') retries: 5 delay: 3 diff --git a/molecule/playbook-rstudio_server-standalone/molecule.yml b/molecule/playbook-rstudio_server-standalone/molecule.yml new file mode 100644 index 00000000..25c0e0dc --- /dev/null +++ b/molecule/playbook-rstudio_server-standalone/molecule.yml @@ -0,0 +1,29 @@ +--- +platforms: + - name: workspace-src-ubuntu_jammy-nginx + image: ghcr.io/utrechtuniversity/src-test-workspace:ubuntu_jammy-nginx + command: /sbin/init + pre_build_image: true + published_ports: + - 8080:80 + registry: + url: $DOCKER_REGISTRY + credentials: + username: $DOCKER_USER + password: $DOCKER_PW +provisioner: + name: ansible + env: + components: + - name: rstudio + path: rstudio_server.yml + parameters: + rstudio_base_path: "/rstudio" + rstudio_standalone: 'true' + jupyterhub_debug: true + jupyterhub_app_pip_executable: uv_pip + jupyterhub_auth: noauth + jupyterhub_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. + jupyterhub_proxy_config: + proxy_set_header: + REMOTE_USER: "$$http_user" # allows us to set the 'user' header in tests to mock login. We need double dollar signs because of the way ansible's `to_yaml` filters escapes! diff --git a/molecule/playbook-rstudio_server-standalone/verify.yml b/molecule/playbook-rstudio_server-standalone/verify.yml new file mode 100644 index 00000000..9d110a72 --- /dev/null +++ b/molecule/playbook-rstudio_server-standalone/verify.yml @@ -0,0 +1,22 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Test rstudio + ansible.builtin.uri: + url: http://localhost/rstudio/ + method: GET + headers: + user: testuser # mock SRAM login + return_content: true + follow_redirects: true + register: rstudio_login + failed_when: rstudio_login.failed or rstudio_login.url != 'http://localhost/rstudio/user/testuser/' + retries: 5 + delay: 3 + + - name: Assert content correct + ansible.builtin.assert: + that: + - '"Posit" in rstudio_login.content' diff --git a/molecule/playbook-rstudio_server/molecule.yml b/molecule/playbook-rstudio_server/molecule.yml new file mode 100644 index 00000000..74084bb4 --- /dev/null +++ b/molecule/playbook-rstudio_server/molecule.yml @@ -0,0 +1,29 @@ +--- +platforms: + - name: workspace-src-ubuntu_jammy-nginx + image: ghcr.io/utrechtuniversity/src-test-workspace:ubuntu_jammy-nginx + command: /sbin/init + pre_build_image: true + published_ports: + - 8080:80 + registry: + url: $DOCKER_REGISTRY + credentials: + username: $DOCKER_USER + password: $DOCKER_PW +provisioner: + name: ansible + env: + components: + - name: jupyterhub + path: jupyterhub.yml + parameters: + jupyter_auth: noauth + jupyter_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. + jupyter_proxy_config: "{ proxy_set_header: { REMOTE_USER: '$$http_user'} }" + - name: rstudio + path: rstudio_server.yml + parameters: + rstudio_base_path: "/rstudio" + rstudio_standalone: 'false' + jupyterhub_app_pip_executable: uv_pip diff --git a/molecule/playbook-rstudio_server/verify.yml b/molecule/playbook-rstudio_server/verify.yml new file mode 100644 index 00000000..fa101318 --- /dev/null +++ b/molecule/playbook-rstudio_server/verify.yml @@ -0,0 +1,22 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Test rstudio + ansible.builtin.uri: + url: http://localhost/hub/spawn/testuser?next=%2Fhub%2Fuser%2Ftestuser%2Frstudio%2F + method: GET + headers: + user: testuser # mock SRAM login + return_content: true + follow_redirects: true + register: rstudio_login + failed_when: rstudio_login.failed or not rstudio_login.url is match ('http://localhost/user/testuser/rstudio') + retries: 5 + delay: 3 + + - name: Assert content correct + ansible.builtin.assert: + that: + - '"Posit" in rstudio_login.content' diff --git a/playbooks/roles/jupyterhub/molecule/base_path/converge.yml b/playbooks/roles/jupyterhub/molecule/base_path/converge.yml new file mode 100644 index 00000000..e43385c4 --- /dev/null +++ b/playbooks/roles/jupyterhub/molecule/base_path/converge.yml @@ -0,0 +1,14 @@ +--- +- name: Converge + hosts: all + gather_facts: true + vars: + jupyterhub_uri: /base_path + jupyterhub_auth: noauth + jupyterhub_activate_remote_user_auth: true # mock SRAM auth by enabling remote user auth in JupyterHub config, despite setting auth to noauth. + jupyterhub_proxy_config: + proxy_set_header: + REMOTE_USER: '$http_user' # allows us to set the 'user' header in tests to mock login + jupyterhub_debug: true + roles: + - role: jupyterhub diff --git a/playbooks/roles/jupyterhub/molecule/base_path/molecule.yml b/playbooks/roles/jupyterhub/molecule/base_path/molecule.yml new file mode 100644 index 00000000..aefd3af0 --- /dev/null +++ b/playbooks/roles/jupyterhub/molecule/base_path/molecule.yml @@ -0,0 +1,26 @@ +--- +driver: + name: ${DRIVER-podman} + image_settings: &image_settings + pre_build_image: true + registry: + url: $DOCKER_REGISTRY + credentials: + username: $DOCKER_USER + password: $DOCKER_PW +provisioner: + name: ansible + playbooks: + prepare: ./prepare.yml + converge: ./converge.yml + env: + ANSIBLE_ROLES_PATH: ../../../ +role_name_check: 1 +platforms: + - name: workspace-src-ubuntu_jammy-nginx + image: ghcr.io/utrechtuniversity/src-test-workspace:ubuntu_jammy-nginx + command: /sbin/init + pre_build_image: true + published_ports: + - 8080:80 + <<: *image_settings diff --git a/playbooks/roles/jupyterhub/molecule/base_path/prepare.yml b/playbooks/roles/jupyterhub/molecule/base_path/prepare.yml new file mode 100644 index 00000000..eda38cb4 --- /dev/null +++ b/playbooks/roles/jupyterhub/molecule/base_path/prepare.yml @@ -0,0 +1,9 @@ +--- +- name: Prepare + hosts: all + gather_facts: true + tasks: + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + when: ansible_pkg_mgr == 'apt' diff --git a/playbooks/roles/jupyterhub/molecule/base_path/verify.yml b/playbooks/roles/jupyterhub/molecule/base_path/verify.yml new file mode 100644 index 00000000..efba6f8b --- /dev/null +++ b/playbooks/roles/jupyterhub/molecule/base_path/verify.yml @@ -0,0 +1,50 @@ +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Test login + ansible.builtin.uri: + url: http://localhost:80/base_path + method: GET + headers: + user: testuser + return_content: true + failed_when: false + register: jupyterhub_login + + - name: Test login with not allowed user + ansible.builtin.uri: + url: http://localhost:80/base_path + method: GET + headers: + user: root + failed_when: false + register: jupyterhub_login_not_allowed + + - name: Test login with not existing user + ansible.builtin.uri: + url: http://localhost:80/base_path + method: GET + headers: + user: foo + failed_when: false + register: jupyterhub_login_not_exist + + - name: Debug 1 + ansible.builtin.debug: + var: jupyterhub_login + + - name: Debug 2 + ansible.builtin.debug: + var: jupyterhub_login_not_allowed + + - name: Debug 3 + ansible.builtin.debug: + var: jupyterhub_login_not_exist + + - name: Assert failed logins + ansible.builtin.assert: + that: + - jupyterhub_login.status == 200 + - jupyterhub_login_not_allowed.status == 500 + - jupyterhub_login_not_exist.status == 500 diff --git a/playbooks/roles/jupyterhub/templates/jupyterhub_config.py.j2 b/playbooks/roles/jupyterhub/templates/jupyterhub_config.py.j2 index f818deb8..8652c74f 100644 --- a/playbooks/roles/jupyterhub/templates/jupyterhub_config.py.j2 +++ b/playbooks/roles/jupyterhub/templates/jupyterhub_config.py.j2 @@ -52,7 +52,7 @@ c.SudoSpawner.sudospawner_path = '{{ jupyterhub_sudospawner_path }}' origin = "*" # can also be the specific URL of the JupyterHub proxy server (e.g. http://localhost:8080) c.Spawner.args = [ - '--NotebookApp.allow_origin={0}'.format(origin), # allow CORS from 'origin' + '--ServerApp.allow_origin={0}'.format(origin), # allow CORS from 'origin' '--transport=ipc' # use unix sockets to communicate with kernels ] c.Spawner.env_keep = {{ jupyterhub_config_env_keep | string }} @@ -63,14 +63,14 @@ c.JupyterHub.hub_bind_url = "unix+http://{{ jupyterhub_user_home | replace("/", c.JupyterHub.hub_connect_url = c.JupyterHub.hub_bind_url c.JupyterHub.ip = "127.0.0.1" # necessary so JupyterHub will start ConfigurableHTTPProxy on the right IP c.Spawner.environment.update({ - 'JUPYTERHUB_API_URL': "http://localhost:8000/hub/api", - 'JUPYTERHUB_ACTIVITY_URL': lambda spawner : f"http://localhost:8000/hub/api/users/{spawner.user.name}/activity" + 'JUPYTERHUB_API_URL': "http://localhost:8000{{ jupyterhub_api_uri }}", + 'JUPYTERHUB_ACTIVITY_URL': lambda spawner : f"http://localhost:8000{{ jupyterhub_api_uri }}/users/{spawner.user.name}/activity" }) {% else %} c.JupyterHub.ip = "{{ jupyterhub_bind_addr }}" c.JypyterHub.port = {{ jupyterhub_port | int }} -c.JupyterHub.base_url = "{{ jupyterhub_uri }}" {% endif %} +c.JupyterHub.base_url = "{{ jupyterhub_uri }}" # Configure paths for essential server files c.JupyterHub.cookie_secret_file = "{{ jupyterhub_user_home }}/jupyterhub_cookie_secret" diff --git a/playbooks/roles/jupyterhub/vars/main.yml b/playbooks/roles/jupyterhub/vars/main.yml index 6489b1d9..2f99d439 100644 --- a/playbooks/roles/jupyterhub/vars/main.yml +++ b/playbooks/roles/jupyterhub/vars/main.yml @@ -8,12 +8,14 @@ jupyterhub_basic_pkgs: - jupyterlab - jhub-remote-user-authenticator - sudospawner +jupyterhub_uri_with_slashes: "{{ '/' ~ (jupyterhub_uri | trim('/')) ~ '/' if jupyterhub_uri != '/' else '/' }}" jupyterhub_default_proxy_config: name: jupyterhub - location: "{{ jupyterhub_uri }}" + location: "{{ jupyterhub_uri_with_slashes }}" proxy_pass: http://{{ jupyterhub_bind_addr }}:{{ jupyterhub_port }} proxy_redirect: http://{{ jupyterhub_bind_addr }}:{{ jupyterhub_port }} $scheme://$host{{ jupyterhub_uri }} auth: "{{ jupyterhub_auth }}" htpasswd: "{{ (jupyterhub_auth == 'basic') | ternary('jupyterhub', omit) }}" auth_sram_header: "{{ jupyterhub_remote_user_header }}" jupyterhub_system_pip: "{{ (jupyterhub_venv | default('system', true)) == 'system' }}" +jupyterhub_api_uri: "{{ jupyterhub_uri_with_slashes }}hub/api" diff --git a/playbooks/roles/jupyterhub_app/meta/main.yml b/playbooks/roles/jupyterhub_app/meta/main.yml index 4bd3e02c..7e0b004f 100644 --- a/playbooks/roles/jupyterhub_app/meta/main.yml +++ b/playbooks/roles/jupyterhub_app/meta/main.yml @@ -11,3 +11,5 @@ galaxy_info: - name: EL versions: - all # not tested for specific versions +dependencies: + - role: system_python diff --git a/playbooks/roles/rstudio/defaults/main.yml b/playbooks/roles/rstudio/defaults/main.yml index bd44a4b6..7e385548 100644 --- a/playbooks/roles/rstudio/defaults/main.yml +++ b/playbooks/roles/rstudio/defaults/main.yml @@ -1,2 +1,3 @@ --- r_version: 4.0.5 +rstudio_kind: desktop diff --git a/playbooks/roles/rstudio/molecule/default/molecule.yml b/playbooks/roles/rstudio/molecule/default/molecule.yml index 617b4deb..be76b672 100644 --- a/playbooks/roles/rstudio/molecule/default/molecule.yml +++ b/playbooks/roles/rstudio/molecule/default/molecule.yml @@ -9,7 +9,7 @@ provisioner: role_name_check: 1 platforms: - name: workspace-src-ubuntu_desktop - image: ghcr.io/utrechtuniversity/src-test-workspace:ubuntu_focal-desktop + image: ghcr.io/utrechtuniversity/src-test-workspace:ubuntu_jammy-desktop pre_build_image: true registry: url: $DOCKER_REGISTRY diff --git a/playbooks/roles/rstudio/molecule/server/converge.yml b/playbooks/roles/rstudio/molecule/server/converge.yml new file mode 100644 index 00000000..278648e9 --- /dev/null +++ b/playbooks/roles/rstudio/molecule/server/converge.yml @@ -0,0 +1,10 @@ +--- +- name: Converge + hosts: all + gather_facts: true + tasks: + - name: Testing rstudio role + ansible.builtin.include_role: + name: rstudio + vars: + rstudio_kind: server diff --git a/playbooks/roles/rstudio/molecule/server/molecule.yml b/playbooks/roles/rstudio/molecule/server/molecule.yml new file mode 100644 index 00000000..8ed76b66 --- /dev/null +++ b/playbooks/roles/rstudio/molecule/server/molecule.yml @@ -0,0 +1,18 @@ +--- +provisioner: + name: ansible + playbooks: + converge: ./converge.yml + prepare: ./prepare.yml + env: + ANSIBLE_ROLES_PATH: ../../../ +role_name_check: 1 +platforms: + - name: workspace-src-ubuntu_desktop + image: ghcr.io/utrechtuniversity/src-test-workspace:ubuntu_jammy + pre_build_image: true + registry: + url: $DOCKER_REGISTRY + credentials: + username: $DOCKER_USER + password: $DOCKER_PW diff --git a/playbooks/roles/rstudio/tasks/debian.yml b/playbooks/roles/rstudio/tasks/debian.yml index 2f5b0b15..adbd99d0 100644 --- a/playbooks/roles/rstudio/tasks/debian.yml +++ b/playbooks/roles/rstudio/tasks/debian.yml @@ -10,14 +10,7 @@ name: r-base-dev state: present -- name: Install RStudio for Ubuntu 20 - when: ansible_distribution == "Ubuntu" and ansible_distribution_version == "20.04" +- name: Install RStudio ansible.builtin.apt: - deb: https://download1.rstudio.org/electron/focal/amd64/rstudio-2024.04.2-764-amd64.deb - state: present - -- name: Install RStudio for Ubuntu 22 - when: ansible_distribution == "Ubuntu" and ansible_distribution_version == "22.04" - ansible.builtin.apt: - deb: https://download1.rstudio.org/electron/jammy/amd64/rstudio-2025.05.0-496-amd64.deb + deb: "{{ rstudio_links['Ubuntu'][ansible_distribution_release][rstudio_kind] }}" state: present diff --git a/playbooks/roles/rstudio/tasks/redhat.yml b/playbooks/roles/rstudio/tasks/redhat.yml index f8a6edfa..8bc01d40 100644 --- a/playbooks/roles/rstudio/tasks/redhat.yml +++ b/playbooks/roles/rstudio/tasks/redhat.yml @@ -4,9 +4,9 @@ name: epel-release state: present -- name: Install R +- name: Install R and RStudio ansible.builtin.package: name: - R - - https://s3.amazonaws.com/rstudio-ide-build/desktop/rhel8/x86_64/rstudio-2022.02.0-443-x86_64.rpm + - "{{ rstudio_links['RedHat'][rstudio_kind] }}" state: present diff --git a/playbooks/roles/rstudio/vars/main.yml b/playbooks/roles/rstudio/vars/main.yml new file mode 100644 index 00000000..e6642695 --- /dev/null +++ b/playbooks/roles/rstudio/vars/main.yml @@ -0,0 +1,13 @@ +rstudio_links: + Ubuntu: + focal: + desktop: https://download1.rstudio.org/electron/focal/amd64/rstudio-2024.04.2-764-amd64.deb + jammy: + desktop: https://download1.rstudio.org/electron/jammy/amd64/rstudio-2025.09.2-418-amd64.deb + server: https://download2.rstudio.org/server/jammy/amd64/rstudio-server-2025.09.2-418-amd64.deb + noble: + desktop: https://download1.rstudio.org/electron/jammy/amd64/rstudio-2025.09.2-418-amd64.deb + server: https://download2.rstudio.org/server/jammy/amd64/rstudio-server-2025.09.2-418-amd64.deb + RedHat: + desktop: https://download1.rstudio.org/electron/rhel9/x86_64/rstudio-2025.09.2-418-x86_64.rpm + server: https://download2.rstudio.org/server/rhel9/x86_64/rstudio-server-rhel-2025.09.2-418-x86_64.rpm diff --git a/playbooks/rstudio_server.yml b/playbooks/rstudio_server.yml new file mode 100644 index 00000000..5e83ba88 --- /dev/null +++ b/playbooks/rstudio_server.yml @@ -0,0 +1,68 @@ +--- +- name: Install RStudio Server in JupyterHub + hosts: localhost + gather_facts: true + vars: + _rstudio_fqdn: "{{ ('molecule-notest' in ansible_skip_tags) | ternary('localhost', rstudio_fqdn) }}" + _rstudio_base_path: "{{ rstudio_base_path | default('/', true) }}" + _rstudio_jupyter_venv_path: "{{ rstudio_jupyter_venv_path | default('/usr/local/jupyterhub', true) }}" + _rstudio_standalone: "{{ rstudio_standalone | default(false, true) | bool }}" # should be set to true unless a previous component has already installed JupyterHub. + pre_tasks: + - name: Get installed R kernels + when: not _rstudio_standalone + block: + - name: Get all kernels + ansible.builtin.command: "{{ _rstudio_jupyter_venv_path }}/bin/jupyter kernelspec list --json" + register: all_kernels_json + changed_when: false + + - name: Get R kernels + ansible.builtin.set_fact: + all_r_kernels: "{{ (all_kernels_json.stdout | from_json)['kernelspecs'].values() | map(attribute='spec') | selectattr('language', 'equalto', 'R') | list }}" + roles: + - role: rstudio + vars: + rstudio_kind: server + - role: jupyterhub_standalone_proxy + when: _rstudio_standalone + # install jupyterhub and rstudio as a standalone app. + vars: + jupyterhub_uri: "{{ _rstudio_base_path }}" + jhsp_external_addr: "{{ _rstudio_fqdn }}" + - role: jupyterhub_app + vars: + jupyterhub_app_name: rstudio + jupyterhub_app_venv: "{{ _rstudio_jupyter_venv_path | default('') }}" + jupyterhub_app_config_dir: "{{ rstudio_jupyter_config_dir | default('') }}" + jupyterhub_app_pip_executable: "{{ rstudio_pip_cmd | default('uv_pip', true) }}" + jupyterhub_app_server_config: | + from jupyter_rsession_proxy import setup_rserver + c.ServerProxy.servers.update({ + {% for server in all_r_kernels | default([]) %} + "rstudio{{ loop.index }}": setup_rserver(prefix="rstudio{{ loop.index }}", r_path="{{ server['argv'][0] }}", launcher_title="RStudio ({{ server['display_name']}})"), + {% endfor %} + }) + jupyterhub_app_standalone_config: | + from utils import * + from jupyter_rsession_proxy import setup_rserver + patch_check_origin() # apply monkeypatch for custom CORS allowlist for websockets + + config = setup_rserver(prefix="") + c.StandaloneProxyServer.unix_socket = True + c.StandaloneProxyServer.command = config['command'] + c.StandaloneProxyServer.rewrite_response = config['rewrite_response'] + c.StandaloneProxyServer.timeout = config['timeout'] + c.StandaloneProxyServer.environment = config['environment'] + + tasks: + - name: Install jupyter-rsession-proxy + notify: Restart JupyterHub + ansible.builtin.pip: + name: jupyter-rsession-proxy + state: present + virtualenv: "{{ _rstudio_jupyter_venv_path }}" + handlers: + - name: Restart JupyterHub + ansible.builtin.service: + state: restarted + name: jupyterhub