Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
.vscode

22 changes: 22 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright (c) 2016-2018 Ohio Supercomputer Center

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,41 @@ A Batch Connect app that launches RStudio Server via a container inside of a Slu
## Creating containers

Versioned RStudio containers from the Rocker project seem to work out-of-the-box:
https://rocker-project.org/images/versioned/rstudio.html
[https://rocker-project.org/images/versioned/rstudio.html](https://rocker-project.org/images/versioned/rstudio.html)

`apptainer pull rocker-rstudio-R_4.6.0-RStudio_TBD-20260512-00.sif docker://rocker/rstudio:4.6.0`
Pull down the container:
```bash
apptainer pull rocker-rstudio-R_4.6.0-RStudio_TBD-20260512-00.sif docker://rocker/rstudio:4.6.0
```

`apptainer shell` the container and find out what version of RStudio it includes. Then update the name to include that, i.e.`rocker-rstudio-R_4.6.0-RStudio_2026.04.0-526-20260512-00.sif`, i.e., `<project>-<image name>-<image version>-R_<version>-RStudio_<version-build>-YYYYMMDD-##.sif` where YYYYMMDD and ## is some optional increment on the image.
Use `apptainer shell` to investigate the container and find out what version of RStudio it includes. Then update the name to include that, i.e., `<project/org>-<image name>-R_<image/R version>-RStudio_<version>-<build>-YYYYMMDD-##.sif`, where YYYYMMDD is the date the image was downloaded and ## is some optional increment on the image, e.g., `rocker-rstudio-R_4.6.0-RStudio_2026.04.0-526-20260512-00.sif`.

If the image is modified, extended, or built by NCSA, then use 'ncsa' as the project/org.

The images can be given "pretty names" in the app interface, but naming the .sif files in a consistent and comprehensive manner is helpful to maintainers.

## Contributing

1. If necessary (no dev privs in ncsa/mg-OOD-RStudio), fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
1. Work on the MG cluster, developing and testing your changes using the developer sandbox mode
2. If necessary (no dev privs in ncsa/mg-OOD-RStudio), fork the repository
3. Create your feature/topic branch (`git checkout -b username/ticket_id/short_desc`)
4. Add and commit your changes (`git commit -am 'Add some feature'`)
4. Push to the feature/topic branch (`git push origin username/ticket_id/short_desc`)
5. Create a new Pull Request

## Publishing containers on the MG cluster

NCSA staff can share containers in `/usr/local/oodimages/rstudio/` for others to use. Make sure permissions are set to allow everyone to access the .sif file.

In order for the app to show the container as an option it must be added in `form.yml.erb`. Be sure to verify that you can select the container and successfully launch the app.

Then put in a PR as described above. After the PR is approved and the branch is merged to `main`, make sure to add an appropriate version tag to the repo.

Finally, ask an admin to update the Open OnDemand server config to pull in this new release by the tag.

## Acknowledgments and References

[https://github.com/sol-eng/singularity-rstudio](https://github.com/sol-eng/singularity-rstudio)\
[https://discourse.openondemand.org/t/rstudio-when-launched-without-singularity-is-having-strange-troubles-with-authentication/1213/60](https://discourse.openondemand.org/t/rstudio-when-launched-without-singularity-is-having-strange-troubles-with-authentication/1213/60)\
[https://github.com/OSC/bc_osc_rstudio_server](https://github.com/OSC/bc_osc_rstudio_server)

93 changes: 93 additions & 0 deletions form.yml.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Batch Connect app configuration file
---

cluster: "magnus"

attributes:

bc_image:
label: "Select from provided container images or custom"
widget: "select"
value: "R 4.6.0"
options:
- ["R 4.6.0","/usr/local/oodimages/rstudio/rocker-rstudio-R_4.6.0-RStudio_2026.04.0-526-20260512-00.sif",data-hide-bc-custom-image: true,data-set-bc-custom-image: ""]
- ["R 4.5.3","/usr/local/oodimages/rstudio/rocker-rstudio-R_4.5.3-RStudio_2026.04.0-526-20260608-00.sif",data-hide-bc-custom-image: true,data-set-bc-custom-image: ""]
- ["custom","custom"]
# cachable: false

bc_custom_image:
label: "Select custom container image"
widget: "path_selector"
help: "Enter the path for your own Apptainer .sif image that contains and R and RStudio. Images from the Rocker project (https://rocker-project.org/images/), or extended from those containers, are likely to work."

bc_reservation:
label: "Name of reservation (leave empty if none)"
widget: "text_field"
value: ""

# bc_partition:
# label: "Partition"
# widget: "select"
# value: "magnus"
# options:
# - ["debug","debug"]
# - ["gpu","gpu"]
# - ["magnus","magnus"]

bc_partition:
label: "Partition"
widget: select
value: "magnus"
options:
## <%- CustomPartitions.partitions.each do |a| %>
## - [ "<%= a.strip %>", "<%= a %>" ]
## <%- end -%>
<%- CustomPartitions.partitions.each do |a| %>
- [ "<%= a.strip %>", "<%= a %>" ]
<%- end -%>
# cacheable: false
### help: "The rstudio partition is available to launch small sessions without delay. Sessions there are restricted to 1 core and a maximum of 8 GB of RAM."

bc_num_slots:
label: "Number of CPUs"
widget: "number_field"
min: "1"
### help: "The rstudio partition only allows single-core sessions."

bc_num_memory:
label: "Amount of RAM"
widget: "text_field"
### help: "Total memory for the node/session (--mem in Slurm). Use Slurm format, e.g., 4096M, 10G. If left blank, 4505 MB will be allocated per CPU core requested. A minimum of 4 GB is required for each job and lower values will be substituted before submission. The rstudio partition allows a maximum of 8 GB."
help: "Total memory for the node/session (--mem in Slurm). Use Slurm format, e.g., 4096M, 10G. If left blank, 4505 MB will be allocated per CPU core requested. A minimum of 4 GB is required for each job and lower values will be substituted before submission."

bc_num_gpus:
label: "Number of GPUs"
# widget: "number_field"
widget: "text_field"
# max: "4"
# min: "0"

bc_num_hours:
label: "Number of hours"
widget: "number_field"
max: "720"
min: "1"

# working_dir:
# label: "Working Directory"
# data-filepicker: true
# data-target-file-type: dirs # Valid values are: files, dirs, or both
# readonly: false
# help: "Select your project directory; defaults to $HOME"

form:
- bc_image
- bc_custom_image
- bc_partition
- bc_num_hours
- bc_reservation
- bc_num_slots
- bc_num_memory
- bc_num_gpus
# - working_dir
- bc_email_on_started
Binary file added icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
name: RStudio Server
category: Interactive Apps
#subcategory: Servers
role: batch_connect
description: |
This app will launch RStudio Server inside of a container running in a Slurm job.
35 changes: 35 additions & 0 deletions submit.yml.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Job submission configuration file
---

batch_connect:
template: "basic"
conn_params:
- csrftoken
native:
script:
queue_name: "<%= bc_partition %>"
email_on_started: <%= bc_email_on_started %>
native:
- "-N 1"
- "-n <%= bc_num_slots.blank? ? 1 : bc_num_slots.to_i %>"
<%- unless bc_num_memory.blank? %>
# make sure the user has requested at least 4 GB of RAM
<%- if ( bc_num_memory.downcase.end_with?('g') and bc_num_memory[/[0-9]+/].to_i < 4 ) or
( bc_num_memory.downcase.end_with?('m') and bc_num_memory[/[0-9]+/].to_i < 4096 ) or
( bc_num_memory.downcase.end_with?('k') and bc_num_memory[/[0-9]+/].to_i < 4194304 ) %>
- "--mem=4G"
<%- else %>
- "--mem=<%= bc_num_memory %>"
<%- end -%>
<%- end -%>
<%- unless bc_num_hours.blank? -%>
- "-t"
- "0-<%= bc_num_hours %>:00:00"
<%- end -%>
<%- unless bc_num_gpus.blank? or bc_num_gpus == 0 -%>
- "--gres=gpu:<%= bc_num_gpus %>"
<%- end -%>
<%- unless bc_reservation.blank? -%>
- "--reservation=<%= bc_reservation %>"
<%- end -%>

9 changes: 9 additions & 0 deletions template/after.sh.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Wait for the RStudio Server to start
echo "Waiting for RStudio Server to open port ${port}..."
if wait_until_port_used "${host}:${port}" 120; then
echo "Discovered RStudio Server listening on port ${port}!"
else
echo "Timed out waiting for RStudio Server to open port ${port}!"
clean_up 1
fi
sleep 2
16 changes: 16 additions & 0 deletions template/before.sh.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Export the module function if it exists
[[ $(type -t module) == "function" ]] && export -f module

# Find available port to run server on
port=$(find_port ${host})

# Define a password and export it for RStudio authentication
password="$(create_passwd 16)"
export RSTUDIO_PASSWORD="${password}"

# create CSRF token
<%-
require 'securerandom'
csrftoken=SecureRandom.uuid
-%>
export csrftoken="<%= csrftoken %>"
27 changes: 27 additions & 0 deletions template/bin/auth
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

# Confirm username is supplied
if [[ $# -lt 1 ]]; then
echo "Usage: auth USERNAME"
exit 1
fi
USERNAME="${1}"

# Confirm password environment variable exists
if [[ -z ${RSTUDIO_PASSWORD} ]]; then
echo "The environment variable RSTUDIO_PASSWORD is not set"
exit 1
fi

# Read in the password from user
read -s -p "Password: " PASSWORD
echo ""

if [[ ${USERNAME} == ${USER} && ${PASSWORD} == ${RSTUDIO_PASSWORD} ]]; then
echo "Successful authentication"
exit 0
else
echo "Invalid authentication"
exit 1
fi

107 changes: 107 additions & 0 deletions template/script.sh.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash

# Load the required environment
setup_env () {
# Additional environment which could be moved into a module
# Change these to suit
export RSTUDIO_SERVER_IMAGE="<% if context.bc_custom_image != '' %><%= context.bc_custom_image %><% else %><%= context.bc_image %><% end %>"
# export APPTAINER_BINDPATH="/etc,/media,/mnt,/opt,/srv,/usr/lib,/lib,/lib64,/usr/apps,/var"
export PATH="$PATH:/usr/lib/rstudio-server/bin"
export APPTAINERENV_PATH="$PATH"
# In Singularity 3.5.x it became necessary to explicitly pass LD_LIBRARY_PATH
# to the singularity process
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/R/4.4.0/lib/R/lib"
# export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-11.3.0/r-4.2.0-2liuw4vmic27cmqhyyt6jmvwbezn6mlx/rlib/R/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/curl-8.0.1-u7dljxx4j2vkwnhrbxl5dnigktopsogc/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/expat-2.5.0-37la23gnxetfy3uytrvk47cmtlycoska/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/geos-3.11.2-ipmlasljmvtuoykqyycrtu5gy4zw47cm/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/json-c-0.16-7lokifwb6lzpml5k63adfgzrbrnc3hi2/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libgeotiff-1.6.0-ql2lodtsyry5cw25ngozc7qtb2jhf6zo/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libjpeg-turbo-2.1.4-cmeyyk3pssdcvl4i4ijmfeoqiubfvby3/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libpng-1.6.39-rpbqrtfdirzf3fojuav2it3lpeqmmnye/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/libtiff-4.4.0-q2c2seunvg5ym3qhgdwe3d3mkr72z5lb/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/proj-8.2.1-t3pclijfh275ljxzranxvzs22hlene5j/lib64:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/sqlite-3.40.1-ux7pnanmrgn4dvcm6qwrbvesyfposm56/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/zlib-1.2.13-smcuvhowz7cwxhb5jcy647kjmrnx4tbc/lib:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-8.5.0/gcc-11.3.0-cwx43q6qt46zl5olgckurx67xtg4nuyd/lib64:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-8.5.0/glib-2.74.6-4wqjtnm3mebxlldhaxpbh4ptqtaxjmoa/lib:/usr/apps/general/spack/sw/linux-rhel8-zen/gcc-11.3.0/hdf5-1.14.0-5xrz3r365nib6fma73irar6fbrgyzhd4/lib:/usr/apps/general/spack/sw/linux-rhel8-cascadelake/gcc-11.3.0/gdal-3.6.3-tkiuujut7ibrj3ljcof2r7zonydhmqt6/lib64"
export APPTAINERENV_LD_LIBRARY_PATH="$LD_LIBRARY_PATH"
}
setup_env
#echo $APPTAINER_BINDPATH
#echo $APPTAINERENV_PATH
#echo $APPTAINERENV_LD_LIBRARY_PATH

#export WORKING_DIR="< % = session_dir % >"
export WORKING_DIR=${PWD}

#
# Start RStudio Server
#

# PAM auth helper used by RStudio
export RSTUDIO_AUTH="${WORKING_DIR}/bin/auth"

# Generate an `rsession` wrapper script
export RSESSION_WRAPPER_FILE="$WORKING_DIR/rsession.sh"
(
umask 077
sed 's/^ \{2\}//' > "$WORKING_DIR/rsession.sh" << EOL
#!/usr/bin/env bash

# Log all output from this script
export RSESSION_LOG_FILE="$WORKING_DIR/rsession.log"

exec &>>"\${RSESSION_LOG_FILE}"
set -x

# rsession.sh doesn't share the same env as the outside script, so these
# need to be set explicitly
export R_LIBS_SITE="${R_LIBS_SITE}"
export RSTUDIO_DATA_HOME="/scratch/rstudio_user_data/${USER}/session_data"
export TZ="US/Eastern"
export HOME="$HOME"
export MODULEPATH_ROOT="$MODULEPATH_ROOT"
export MODULEPATH="$MODULEPATH"
export LMOD_PKG="$LMOD_PKG"

env

# Launch the original command
echo "Launching rsession..."
echo "...with args..."
echo "\${@}"
exec rsession --r-libs-user "${R_LIBS_USER}" "\${@}"
EOL
)
chmod 700 "$WORKING_DIR/rsession.sh"

# Create DB config file
(
umask 077
sed 's/^ \{2\}//' > "$WORKING_DIR/database.conf" << EOL
provider=sqlite
directory="$WORKING_DIR"
exec rsession --r-libs-user "${R_LIBS_USER}" "\${@}"
EOL
)
chmod 700 "$WORKING_DIR/database.conf"

# Set working directory to home directory
cd "${HOME}"

# server log directory in this job's working directory
mkdir -p "${WORKING_DIR}/logs"
(
umask 077
sed 's/^ \{2\}//' > "$WORKING_DIR/logging.conf" << EOL
[*]
log-dir="${WORKING_DIR}/logs"
EOL
)
chmod 700 "$WORKING_DIR/logging.conf"

export TMPDIR="$(mktemp -d)"
#TMPDIR="${WORKING_DIR}/tmp"
#mkdir -p $TMPDIR

mkdir -p "$TMPDIR/rstudio-server"
python3 -c 'from uuid import uuid4; print(uuid4())' > "$TMPDIR/rstudio-server/secure-cookie-key"
chmod 0600 "$TMPDIR/rstudio-server/secure-cookie-key"

module purge

set -x
# Launch the RStudio Server
echo "Starting up rserver..."

apptainer run --no-mount /usr/apps -B "$TMPDIR:/tmp,${WORKING_DIR}/logging.conf:/etc/rstudio/logging.conf" "$RSTUDIO_SERVER_IMAGE" rserver --www-port ${port} --auth-none 0 --auth-pam-helper-path "${RSTUDIO_AUTH}" --auth-encrypt-password 0 --rsession-path "${RSESSION_WRAPPER_FILE}" --server-data-dir="${TMPDIR}/run" --secure-cookie-key-file "${TMPDIR}/rstudio-server/secure-cookie-key" --server-user=$(whoami) --database-config-file="$WORKING_DIR/database.conf"

echo 'Singularity has exited...'
20 changes: 20 additions & 0 deletions view.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script type="text/javascript">
(function () {
let d = new Date();
d.setTime(d.getTime() + (7*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
let cookie = `csrf-token=<%= csrftoken %>;${expires};path=/rnode/<%= host %>/<%= port %>/;SameSite=Strict;Secure;`
document.cookie = cookie;
})();

</script>
<form action="/rnode/<%= host %>/<%= port %>/auth-do-sign-in" method="post" target="_blank">
<input type="hidden" name="username" value="<%= ENV["USER"] %>">
<input type="hidden" name="password" value="<%= password %>">
<input type="hidden" name="staySignedIn" value="1">
<input type="hidden" name="appUri" value="">
<input id="csrfToken" type="hidden" name="csrf-token" value="<%= csrftoken %>"/>
<button class="btn btn-primary" type="submit">
<i class="fa fa-registered"></i> Connect to RStudio
</button>
</form>