DSP Project first push, date: 29/01/2026
This commit is contained in:
9
docker/jupyterhub/Dockerfile
Normal file
9
docker/jupyterhub/Dockerfile
Normal 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
|
||||
144
docker/jupyterhub/jupyterhub_config.py
Normal file
144
docker/jupyterhub/jupyterhub_config.py
Normal 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.")
|
||||
Reference in New Issue
Block a user