This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new ca1f42224 Add airavata-jupyterhub (moved from apache/airavata) (#110)
ca1f42224 is described below

commit ca1f422241a0b018261d9d9b7d06da081b1acf80
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Sun Jun 7 17:21:47 2026 -0400

    Add airavata-jupyterhub (moved from apache/airavata) (#110)
    
    Moves the JupyterHub deployment config (Dockerfiles, jupyterhub_config.py, 
login template and user container) out of the airavata middleware repo and into 
this portals monorepo, where the gateway-facing deployment assets belong; also 
lists it in the Sub-Projects table.
---
 CLAUDE.md                                       |   1 +
 airavata-jupyterhub/Dockerfile                  |  14 +++
 airavata-jupyterhub/custom_templates/login.html |  52 +++++++++
 airavata-jupyterhub/docker-compose.yaml         |  24 ++++
 airavata-jupyterhub/jupyterhub_config.py        | 139 ++++++++++++++++++++++++
 airavata-jupyterhub/user-container/Dockerfile   |  12 ++
 airavata-jupyterhub/user-container/Makefile     |   3 +
 airavata-jupyterhub/user-container/init.sh      |  28 +++++
 8 files changed, 273 insertions(+)

diff --git a/CLAUDE.md b/CLAUDE.md
index da58e4715..564e66476 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,6 +18,7 @@ Monorepo of web portals, SDKs, and tools built on top of 
Apache Airavata. Contai
 | `airavata-mft-portal` | Django + Webpack | Managed File Transfer dashboard |
 | `airavata-local-agent` | Electron + Next.js | Desktop app for local Docker 
container management |
 | `airavata-mcp-client-chatbot` | Flask + React | MCP-based chatbot for 
querying CyberShuttle resources |
+| `airavata-jupyterhub` | JupyterHub + Docker | JupyterHub deployment for 
gateway notebook sessions (config, images, user container) |
 | `airavata-cookiecutter-django-app` | Cookiecutter | Template for scaffolding 
custom Django apps |
 | `airavata-cookiecutter-django-output-view` | Cookiecutter | Template for 
output viewer plugins |
 | `airavata-php-gateway` | PHP | Legacy gateway (archived, replaced by Django 
portal) |
diff --git a/airavata-jupyterhub/Dockerfile b/airavata-jupyterhub/Dockerfile
new file mode 100644
index 000000000..d0f930078
--- /dev/null
+++ b/airavata-jupyterhub/Dockerfile
@@ -0,0 +1,14 @@
+FROM jupyterhub/jupyterhub:latest
+
+RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
+RUN pip install oauthenticator requests pyjwt dockerspawner 
jupyterhub-idle-culler ipywidgets
+
+COPY jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py
+COPY custom_templates /srv/jupyterhub/custom_templates
+
+ENV JUPYTERHUB_CONFIG=/srv/jupyterhub/jupyterhub_config.py
+ENV PYTHONPATH=/srv/jupyterhub
+
+EXPOSE 20000
+
+CMD ["jupyterhub"]
\ No newline at end of file
diff --git a/airavata-jupyterhub/custom_templates/login.html 
b/airavata-jupyterhub/custom_templates/login.html
new file mode 100644
index 000000000..d7834044c
--- /dev/null
+++ b/airavata-jupyterhub/custom_templates/login.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>CyberShuttle Hub Login</title>
+    <link rel="icon" href="{{ static_url('images/airavata-logo.png') }}">
+    <link rel="stylesheet" href="{{ static_url('css/style.min.css') }}">
+    <style>
+        body {
+            font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+            background-color: #f7f9fc;
+            color: #333;
+            text-align: center;
+            padding-top: 80px;
+        }
+        .container {
+            display: inline-block;
+            padding: 50px;
+            border-radius: 8px;
+            background-color: #ffffff;
+            box-shadow: 0 3px 8px rgba(0,0,0,0.1);
+        }
+        img {
+            width: 70px;
+            margin-bottom: 20px;
+        }
+        h1 {
+            font-size: 26px;
+            margin-bottom: 20px;
+        }
+        a.btn-login {
+            display: inline-block;
+            background-color: #0d6efd;
+            color: #ffffff;
+            padding: 10px 20px;
+            border-radius: 5px;
+            text-decoration: none;
+            font-size: 16px;
+        }
+        a.btn-login:hover {
+            background-color: #0b5ed7;
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <img src="https://airavata.host:8009/static/images/airavata-logo.png"; 
alt="CyberShuttle Logo"/>
+    <h1>Welcome to Dev CyberShuttle Hub</h1>
+    <a class="btn-login" href="{{ authenticator_login_url }}">Login with OAuth 
2.0</a>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/airavata-jupyterhub/docker-compose.yaml 
b/airavata-jupyterhub/docker-compose.yaml
new file mode 100644
index 000000000..568cf9702
--- /dev/null
+++ b/airavata-jupyterhub/docker-compose.yaml
@@ -0,0 +1,24 @@
+services:
+  jupyterhub:
+    build: .
+    container_name: jupyterhub
+    ports:
+      - "8000:20000"
+    environment:
+      OAUTH_CLIENT_ID: "cs-jupyterlab"
+      OAUTH_CLIENT_SECRET: "DxeMtfiWU1qkDEmaGHf13RDahCujzhy1"
+      JUPYTERHUB_CRYPT_KEY: 
"a99323294a5d6f9b1d0e7e33450dff44db664264231b985e069c6eba8f9a3e09"
+      DOCKER_NETWORK_NAME: jupyterhub_network
+      DOCKER_NOTEBOOK_IMAGE: cybershuttle/dev_jupyterlab-base
+    volumes:
+      - ./jupyterlab:/home/jovyan
+      - ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py
+      - ./custom_templates:/srv/jupyterhub/custom_templates
+      - /var/run/docker.sock:/var/run/docker.sock
+    restart: always
+    networks:
+      - jupyterhub_network
+
+networks:
+  jupyterhub_network:
+    name: jupyterhub_network
\ No newline at end of file
diff --git a/airavata-jupyterhub/jupyterhub_config.py 
b/airavata-jupyterhub/jupyterhub_config.py
new file mode 100644
index 000000000..ec3958475
--- /dev/null
+++ b/airavata-jupyterhub/jupyterhub_config.py
@@ -0,0 +1,139 @@
+import os
+import re
+import sys
+from dockerspawner import DockerSpawner
+from oauthenticator.generic import GenericOAuthenticator
+from traitlets.config import Config
+
+c: Config
+
+# Authenticator Configuration
+c.JupyterHub.authenticator_class = GenericOAuthenticator
+c.GenericOAuthenticator.client_id = os.getenv('OAUTH_CLIENT_ID')
+c.GenericOAuthenticator.client_secret = os.getenv('OAUTH_CLIENT_SECRET')
+c.GenericOAuthenticator.oauth_callback_url = 
'http://airavata.host:20000/hub/oauth_callback'
+c.GenericOAuthenticator.authorize_url = 
'http://airavata.host:18080/realms/default/protocol/openid-connect/auth'
+c.GenericOAuthenticator.token_url = 
'http://airavata.host:18080/realms/default/protocol/openid-connect/token'
+c.GenericOAuthenticator.userdata_url = 
'http://airavata.host:18080/realms/default/protocol/openid-connect/userinfo'
+c.GenericOAuthenticator.scope = ['openid', 'profile', 'email']
+c.GenericOAuthenticator.username_claim = 'email'
+
+# User Permissions
+c.Authenticator.enable_auth_state = True
+c.GenericOAuthenticator.allow_all = True
+c.Authenticator.admin_users = {'[email protected]'}
+
+
+# Custom Spawner
+class CustomDockerSpawner(DockerSpawner):
+
+    def _options_form_default(self):
+        return ""
+
+    def options_from_form(self, formdata):
+        options = {}
+
+        if hasattr(self, 'handler') and self.handler:
+            qs_args = self.handler.request.arguments  # eg. {'git': 
[b'https://github...'], 'dataPath': [b'bmtk']}
+            if 'git' in qs_args:
+                options['git'] = qs_args['git'][0].decode('utf-8')
+            if 'dataPath' in qs_args:
+                # decode ALL dataPath values into a list of strings
+                options['dataPath'] = [v.decode('utf-8') for v in 
qs_args['dataPath']]
+
+        return options
+
+    def sanitize_name(self, name):
+        """Docker safe volume/container names."""
+        return re.sub(r"[^a-zA-Z0-9_.-]", "_", name)
+
+    async def start(self):
+        # Create a unique volume name keyed by (username + servername).
+        # If the user spawns again with the same (servername), it will reuse 
the same volume.
+        safe_user = self.sanitize_name(self.user.name)
+        safe_srv = self.sanitize_name(self.name or "default")
+        reference_git_url = 
"https://github.com/cyber-shuttle/cybershuttle-reference.git";
+        git_url = self.user_options.get("git") or reference_git_url
+        data_subfolders = self.user_options.get("dataPath", [])
+        print("THE DATA PATH IS: ", data_subfolders)
+
+        self.image = "cybershuttle/jupyterlab-base"
+        if not hasattr(self, "environment"):
+            self.environment = {}
+        self.environment["GIT_URL"] = git_url
+        self.post_start_cmd = "/usr/local/bin/init.sh"
+        self.volumes = {}
+
+        # register the home directory as volume (rw)
+        vol_name = f"jupyterhub-vol-{safe_user}-{safe_srv}"
+        self.volumes[vol_name] = "/home/jovyan/work"
+
+        # register given datasets as volumes (ro)
+        for subfolder in data_subfolders:
+            host_data_path = f"/mnt/{subfolder}"
+            container_path = f"/cybershuttle_data/{subfolder}"
+            self.volumes[host_data_path] = {
+                'bind': container_path,
+                'mode': 'ro'
+            }
+
+        return await super().start()
+
+
+# Spawner Configuration
+c.JupyterHub.allow_named_servers = True
+c.JupyterHub.named_server_limit_per_user = 10
+c.JupyterHub.spawner_class = CustomDockerSpawner
+c.DockerSpawner.notebook_dir = '/home/jovyan/work'
+c.DockerSpawner.default_url = "/lab"
+c.DockerSpawner.start_timeout = 600
+
+c.DockerSpawner.environment = {
+    'CHOWN_HOME': 'no',
+    'CHOWN_HOME_OPTS': '',
+}
+c.DockerSpawner.extra_create_kwargs = {'user': 'root'}
+c.DockerSpawner.use_internal_ip = True
+c.DockerSpawner.network_name = os.getenv('DOCKER_NETWORK_NAME', 
'jupyterhub_network')
+
+# Hub Configuration
+c.JupyterHub.hub_ip = '0.0.0.0'
+c.JupyterHub.hub_port = 8081
+c.JupyterHub.hub_connect_ip = 'jupyterhub'
+c.JupyterHub.shutdown_on_logout = True
+
+# External URL
+c.JupyterHub.external_url = 'http://airavata.host:20000'
+
+# Logging
+c.JupyterHub.log_level = 'DEBUG'
+
+# Terminate idle notebook containers
+c.JupyterHub.services = [
+    {
+        "name": "jupyterhub-idle-culler-service",
+        "admin": True,
+        "command": [sys.executable, "-m", "jupyterhub_idle_culler", 
"--timeout=3600"],
+    }
+]
+
+c.JupyterHub.load_roles = [
+    {
+        "name": "jupyterhub-idle-culler-role",
+        "scopes": [
+            "list:users",
+            "read:users:activity",
+            "read:servers",
+            "delete:servers",
+        ],
+        "services": ["jupyterhub-idle-culler-service"],
+    }
+]
+
+# SSL Termination
+c.JupyterHub.bind_url = 'http://0.0.0.0:20000'
+c.JupyterHub.external_ssl = True
+
+# Custom templates - Login
+c.JupyterHub.template_paths = ['/srv/jupyterhub/custom_templates']
+c.OAuthenticator.login_service = "Sign in with Existing Institution 
Credentials"
diff --git a/airavata-jupyterhub/user-container/Dockerfile 
b/airavata-jupyterhub/user-container/Dockerfile
new file mode 100644
index 000000000..26bf413f9
--- /dev/null
+++ b/airavata-jupyterhub/user-container/Dockerfile
@@ -0,0 +1,12 @@
+FROM quay.io/jupyter/base-notebook:latest
+
+COPY init.sh /usr/local/bin/init.sh
+
+USER root
+RUN chmod +x /usr/local/bin/init.sh
+RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
+
+USER $NB_USER
+RUN pip install ipywidgets jupyterlab-git
+
+CMD ["start-notebook.py"]
\ No newline at end of file
diff --git a/airavata-jupyterhub/user-container/Makefile 
b/airavata-jupyterhub/user-container/Makefile
new file mode 100755
index 000000000..4f74d1c5f
--- /dev/null
+++ b/airavata-jupyterhub/user-container/Makefile
@@ -0,0 +1,3 @@
+deploy:
+       docker build --platform linux/x86_64 -t cybershuttle/jupyterlab-base . 
&& \
+       docker push cybershuttle/jupyterlab-base
\ No newline at end of file
diff --git a/airavata-jupyterhub/user-container/init.sh 
b/airavata-jupyterhub/user-container/init.sh
new file mode 100755
index 000000000..9eb18ec39
--- /dev/null
+++ b/airavata-jupyterhub/user-container/init.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+TARGET_DIR="/home/jovyan/work"
+SHARED_TMP="/cybershuttle_data"
+
+mkdir -p "$TARGET_DIR"
+
+if [ ! -f "$TARGET_DIR/.initialized" ]; then
+    chown -R jovyan:users "$TARGET_DIR"
+
+    # If $GIT_URL is set, clone the repo into the workspace
+    if [ -n "$GIT_URL" ]; then
+        echo "Cloning repo from $GIT_URL..."
+        cd "$TARGET_DIR"
+        git clone "$GIT_URL" .
+        chown -R jovyan:users .
+    fi
+    touch "$TARGET_DIR/.initialized"
+else
+    echo "Docker default files already exist, skipping copy."
+fi
+
+if [ -d "$SHARED_TMP" ]; then
+    echo "Linking shared data to workspace..."
+    ln -s "$SHARED_TMP" "$TARGET_DIR/cybershuttle_data"
+fi
+
+exec "$@"

Reply via email to