DSP Project first push, date: 29/01/2026

This commit is contained in:
Sok Ponlork
2026-01-29 14:31:48 +07:00
parent 951262afb3
commit 644b624d2d
1857 changed files with 163516 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
FROM jupyterhub/jupyterhub:4.1
# Install required Python packages and Docker CLI for pre-spawn syncing
RUN pip install --no-cache-dir oauthenticator dockerspawner jupyterhub-idle-culler && \
apt-get update && \
apt-get install -y --no-install-recommends docker.io && \
apt-get clean && rm -rf /var/lib/apt/lists/*
COPY jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py

View File

@@ -0,0 +1,144 @@
import logging
import os
import re
import subprocess
from typing import Dict, Optional
from oauthenticator.generic import GenericOAuthenticator
from jupyterhub.auth import DummyAuthenticator
from dockerspawner import DockerSpawner
c = get_config()
# Authenticator selection (environment-driven)
c.Authenticator.enable_auth_state = False
auth_strategy = os.environ.get("JUPYTERHUB_AUTH_STRATEGY", "oauth").strip().lower()
if auth_strategy == "dummy":
c.JupyterHub.authenticator_class = DummyAuthenticator
dummy_password = os.environ.get("JUPYTERHUB_DUMMY_PASSWORD")
if dummy_password:
c.DummyAuthenticator.password = dummy_password
else:
c.JupyterHub.authenticator_class = GenericOAuthenticator
c.GenericOAuthenticator.client_id = os.environ.get("DSP_OAUTH_CLIENT_ID", "")
c.GenericOAuthenticator.client_secret = os.environ.get("DSP_OAUTH_CLIENT_SECRET", "")
c.GenericOAuthenticator.authorize_url = os.environ.get("DSP_OAUTH_AUTHORIZE_URL", "")
c.GenericOAuthenticator.token_url = os.environ.get("DSP_OAUTH_TOKEN_URL", "")
c.GenericOAuthenticator.userdata_url = os.environ.get("DSP_OAUTH_USERINFO_URL", "")
c.GenericOAuthenticator.oauth_callback_url = os.environ.get("JUPYTERHUB_OAUTH_CALLBACK", "")
c.GenericOAuthenticator.scope = ["profile"]
c.GenericOAuthenticator.username_claim = "hub_username"
c.GenericOAuthenticator.username_key = "hub_username"
c.GenericOAuthenticator.auto_login = True
# Explicitly acknowledge HTTP when running behind an external TLS terminator.
c.JupyterHub.confirm_no_ssl = True
c.Spawner.http_timeout = int(os.getenv("JUPYTERHUB_HTTP_TIMEOUT", "90"))
c.Spawner.start_timeout = int(os.getenv("JUPYTERHUB_START_TIMEOUT", "90"))
def _env_bool(name: str, default: bool = False) -> bool:
value = os.getenv(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
tornado_settings = getattr(c.JupyterHub, "tornado_settings", {})
if isinstance(tornado_settings, dict):
merged_settings = tornado_settings.copy()
else:
merged_settings = {}
default_frame_ancestors = ["'self'", "http://localhost:8082", "http://127.0.0.1:8082"]
app_origin_values = [value.rstrip("/") for value in os.getenv("DSP_APP_ORIGINS", "").split() if value]
extra_frame_ancestors = [value.rstrip("/") for value in os.getenv("DSP_FRAME_ANCESTORS", "").split() if value]
frame_ancestors = " ".join(
dict.fromkeys(default_frame_ancestors + app_origin_values + extra_frame_ancestors)
)
header_settings = {
"Content-Security-Policy": f"frame-ancestors {frame_ancestors}",
"X-Frame-Options": "ALLOWALL",
}
existing_headers = merged_settings.get("headers", {})
existing_headers.update(header_settings)
merged_settings["headers"] = existing_headers
external_url = os.getenv("JUPYTER_EXTERNAL_URL", "")
cookie_secure_default = external_url.startswith("https://")
merged_settings["cookie_options"] = {
"SameSite": "None",
"Secure": _env_bool("JUPYTERHUB_COOKIE_SECURE", cookie_secure_default),
}
c.JupyterHub.tornado_settings = merged_settings
# Single-user server configuration
c.JupyterHub.spawner_class = DockerSpawner
c.DockerSpawner.image = os.environ.get("DSP_JH_IMAGE", "jupyter/minimal-notebook:python-3.11")
c.DockerSpawner.remove_containers = True
c.DockerSpawner.cmd = ["start-singleuser.sh"]
c.DockerSpawner.notebook_dir = "/home/jovyan/work"
c.DockerSpawner.network_name = os.environ.get("DSP_JH_NETWORK", "dsp_default")
def _workspace_volume(username: str) -> Dict[str, str]:
safe = re.sub(r"[^a-zA-Z0-9._-]+", "-", username)
host_root = os.environ.get("DSP_WORKSPACE_ROOT", "/var/www/html/uploads/jupyter_workspace")
volumes: Dict[str, str] = {f"{host_root}/{safe}": "/home/jovyan/work"}
r_scripts_root = os.environ.get("DSP_R_SCRIPTS_ROOT", "/var/www/html/r_scripts")
if os.path.isdir(r_scripts_root):
volumes[r_scripts_root] = "/home/jovyan/work/r_scripts"
return volumes
def _extract_person_id(username: str) -> Optional[str]:
match = re.search(r"(\d+)$", username or "")
return match.group(1) if match else None
def _run_sync(person_id: str) -> None:
command = [
"docker",
"exec",
os.environ.get("DSP_APP_CONTAINER", "dsp_app"),
"php",
"/var/www/html/scripts/trigger_workspace_sync.php",
person_id,
]
subprocess.run(command, check=False)
async def pre_spawn_hook(spawner):
username = spawner.user.name
spawner.volumes = _workspace_volume(username)
person_id = _extract_person_id(username)
if person_id:
_run_sync(person_id)
c.DockerSpawner.pre_spawn_hook = pre_spawn_hook
_cull_token = os.environ.get("JUPYTERHUB_CULL_API_TOKEN")
if _cull_token:
c.JupyterHub.services = [
{
"name": "cull-idle",
"command": [
"python",
"-m",
"jupyterhub_idle_culler",
"--timeout=3600",
"--cull-every=600",
"--concurrency=10",
],
"api_token": _cull_token,
"admin": True,
}
]
else:
logging.warning("JUPYTERHUB_CULL_API_TOKEN not set; idle culler disabled.")