mirror of
https://github.com/stellarshenson/stellars-jupyterhub-ds.git
synced 2026-03-12 07:55:06 +00:00
feat: externalize settings metadata to YAML dictionary
- Add config/settings_dictionary.yml with category-based structure - Categories as top-level keys (JupyterHub Core, Docker Spawner, etc.) - Each setting has name, description, default, optional empty_display - Update SettingsPageHandler to load from YAML file - Add pyyaml to Dockerfile pip dependencies - Refactor pip install to heredoc format with echo message
This commit is contained in:
@@ -144,3 +144,6 @@ This journal tracks substantive work on documents, diagrams, and documentation c
|
||||
|
||||
47. **Task - Standardize env vars with JUPYTERHUB_ prefix**: Renamed all environment variables to use consistent JUPYTERHUB_ prefix and added admin Settings page<br>
|
||||
**Result**: Renamed 13 environment variables (ENABLE_GPU_SUPPORT->JUPYTERHUB_GPU_ENABLED, ENABLE_JUPYTERHUB_SSL->JUPYTERHUB_SSL_ENABLED, ENABLE_SERVICE_*->JUPYTERHUB_SERVICE_*, ENABLE_SIGNUP->JUPYTERHUB_SIGNUP_ENABLED, DOCKER_NOTEBOOK_IMAGE->JUPYTERHUB_NOTEBOOK_IMAGE, DOCKER_NETWORK_NAME->JUPYTERHUB_NETWORK_NAME, NVIDIA_AUTODETECT_IMAGE->JUPYTERHUB_NVIDIA_IMAGE, IDLE_CULLER_*->JUPYTERHUB_IDLE_CULLER_*). Created SettingsPageHandler with admin-only access at /settings showing read-only banded table of all configuration values. Updated compose.yml, jupyterhub_config.py, custom_handlers.py, README.md, CLAUDE.md. Added Settings link to admin navbar. No backward compatibility for old names. Bumped version to 3.6.1
|
||||
|
||||
48. **Task - Settings dictionary YAML**: Externalized settings metadata to config/settings_dictionary.yml<br>
|
||||
**Result**: Created settings_dictionary.yml with categories as top-level keys (JupyterHub Core, Docker Spawner, GPU, Services, Idle Culler, Branding), each containing list of settings with name, description, default, and optional empty_display. Updated SettingsPageHandler to load from YAML instead of hardcoded values. Added pyyaml to Dockerfile pip install. Dockerfile now copies settings_dictionary.yml to /srv/jupyterhub/
|
||||
|
||||
79
config/settings_dictionary.yml
Normal file
79
config/settings_dictionary.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
# Settings Dictionary
|
||||
# Defines environment variables displayed on the admin Settings page
|
||||
# Categories are top-level keys, each containing a list of settings
|
||||
# Each setting has: name (env var), description (UI label), default (fallback value)
|
||||
|
||||
JupyterHub Core:
|
||||
- name: JUPYTERHUB_ADMIN
|
||||
description: Admin username
|
||||
default: admin
|
||||
|
||||
- name: JUPYTERHUB_BASE_URL
|
||||
description: Base URL path
|
||||
default: /jupyterhub
|
||||
|
||||
- name: JUPYTERHUB_SIGNUP_ENABLED
|
||||
description: User self-registration (0=disabled, 1=enabled)
|
||||
default: "1"
|
||||
|
||||
- name: JUPYTERHUB_SSL_ENABLED
|
||||
description: SSL/TLS (0=disabled, 1=enabled)
|
||||
default: "1"
|
||||
|
||||
Docker Spawner:
|
||||
- name: JUPYTERHUB_NOTEBOOK_IMAGE
|
||||
description: User container image
|
||||
default: stellars/stellars-jupyterlab-ds:latest
|
||||
|
||||
- name: JUPYTERHUB_NETWORK_NAME
|
||||
description: Docker network for containers
|
||||
default: jupyterhub_network
|
||||
|
||||
GPU:
|
||||
- name: JUPYTERHUB_GPU_ENABLED
|
||||
description: GPU support (0=disabled, 1=enabled, 2=auto-detect)
|
||||
default: "2"
|
||||
|
||||
- name: JUPYTERHUB_NVIDIA_IMAGE
|
||||
description: CUDA image for GPU detection
|
||||
default: nvidia/cuda:12.9.1-base-ubuntu24.04
|
||||
|
||||
Services:
|
||||
- name: JUPYTERHUB_SERVICE_MLFLOW
|
||||
description: MLflow service (0=disabled, 1=enabled)
|
||||
default: "1"
|
||||
|
||||
- name: JUPYTERHUB_SERVICE_GLANCES
|
||||
description: Glances service (0=disabled, 1=enabled)
|
||||
default: "1"
|
||||
|
||||
- name: JUPYTERHUB_SERVICE_TENSORBOARD
|
||||
description: TensorBoard service (0=disabled, 1=enabled)
|
||||
default: "1"
|
||||
|
||||
Idle Culler:
|
||||
- name: JUPYTERHUB_IDLE_CULLER_ENABLED
|
||||
description: Idle culler (0=disabled, 1=enabled)
|
||||
default: "0"
|
||||
|
||||
- name: JUPYTERHUB_IDLE_CULLER_TIMEOUT
|
||||
description: Idle timeout in seconds
|
||||
default: "86400"
|
||||
|
||||
- name: JUPYTERHUB_IDLE_CULLER_INTERVAL
|
||||
description: Check interval in seconds
|
||||
default: "600"
|
||||
|
||||
- name: JUPYTERHUB_IDLE_CULLER_MAX_AGE
|
||||
description: Max server age (0=unlimited)
|
||||
default: "0"
|
||||
|
||||
Branding:
|
||||
- name: JUPYTERHUB_CUSTOM_LOGO_URI
|
||||
description: Custom logo URI
|
||||
default: ""
|
||||
empty_display: "(default)"
|
||||
|
||||
- name: STELLARS_JUPYTERHUB_VERSION
|
||||
description: Platform version
|
||||
default: unknown
|
||||
@@ -42,13 +42,18 @@ COPY --chmod=600 services/jupyterhub/templates/certs /mnt/certs
|
||||
COPY --chmod=644 services/jupyterhub/templates_enhanced/*.html /srv/jupyterhub/templates/
|
||||
COPY --chmod=644 services/jupyterhub/templates_enhanced/static/custom.css /tmp/custom.css
|
||||
COPY --chmod=644 config/jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py
|
||||
COPY --chmod=644 config/settings_dictionary.yml /srv/jupyterhub/settings_dictionary.yml
|
||||
|
||||
## install dockerspawner, nativeauthenticator, idle-culler
|
||||
RUN pip install -U --no-cache-dir \
|
||||
docker \
|
||||
dockerspawner \
|
||||
jupyterhub-nativeauthenticator \
|
||||
jupyterhub-idle-culler
|
||||
## install dockerspawner, nativeauthenticator, idle-culler, pyyaml
|
||||
RUN <<-EOF
|
||||
echo "installing core jupyterhub python packages"
|
||||
pip install -U --no-cache-dir \
|
||||
docker \
|
||||
dockerspawner \
|
||||
jupyterhub-nativeauthenticator \
|
||||
jupyterhub-idle-culler \
|
||||
pyyaml
|
||||
EOF
|
||||
|
||||
## copy custom.css to JupyterHub's static directory
|
||||
RUN <<-EOF
|
||||
|
||||
@@ -566,6 +566,9 @@ class GetUserCredentialsHandler(BaseHandler):
|
||||
class SettingsPageHandler(BaseHandler):
|
||||
"""Handler for rendering the settings page (admin only, read-only)"""
|
||||
|
||||
# Settings dictionary file path
|
||||
SETTINGS_DICT_PATH = "/srv/jupyterhub/settings_dictionary.yml"
|
||||
|
||||
@web.authenticated
|
||||
async def get(self):
|
||||
"""
|
||||
@@ -581,38 +584,48 @@ class SettingsPageHandler(BaseHandler):
|
||||
|
||||
self.log.info(f"[Settings Page] Admin {current_user.name} accessed settings panel")
|
||||
|
||||
# Collect environment variables and settings
|
||||
settings = []
|
||||
|
||||
# JupyterHub Core Settings
|
||||
settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_ADMIN", "value": os.environ.get("JUPYTERHUB_ADMIN", "admin"), "description": "Admin username"})
|
||||
settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_BASE_URL", "value": os.environ.get("JUPYTERHUB_BASE_URL", "/jupyterhub"), "description": "Base URL path"})
|
||||
settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_SIGNUP_ENABLED", "value": os.environ.get("JUPYTERHUB_SIGNUP_ENABLED", "1"), "description": "User self-registration (0=disabled, 1=enabled)"})
|
||||
settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_SSL_ENABLED", "value": os.environ.get("JUPYTERHUB_SSL_ENABLED", "1"), "description": "SSL/TLS (0=disabled, 1=enabled)"})
|
||||
|
||||
# Docker/Spawner Settings
|
||||
settings.append({"category": "Docker Spawner", "name": "JUPYTERHUB_NOTEBOOK_IMAGE", "value": os.environ.get("JUPYTERHUB_NOTEBOOK_IMAGE", "stellars/stellars-jupyterlab-ds:latest"), "description": "User container image"})
|
||||
settings.append({"category": "Docker Spawner", "name": "JUPYTERHUB_NETWORK_NAME", "value": os.environ.get("JUPYTERHUB_NETWORK_NAME", "jupyterhub_network"), "description": "Docker network for containers"})
|
||||
|
||||
# GPU Settings
|
||||
settings.append({"category": "GPU", "name": "JUPYTERHUB_GPU_ENABLED", "value": os.environ.get("JUPYTERHUB_GPU_ENABLED", "2"), "description": "GPU support (0=disabled, 1=enabled, 2=auto-detect)"})
|
||||
settings.append({"category": "GPU", "name": "JUPYTERHUB_NVIDIA_IMAGE", "value": os.environ.get("JUPYTERHUB_NVIDIA_IMAGE", "nvidia/cuda:12.9.1-base-ubuntu24.04"), "description": "CUDA image for GPU detection"})
|
||||
|
||||
# Services
|
||||
settings.append({"category": "Services", "name": "JUPYTERHUB_SERVICE_MLFLOW", "value": os.environ.get("JUPYTERHUB_SERVICE_MLFLOW", "1"), "description": "MLflow service (0=disabled, 1=enabled)"})
|
||||
settings.append({"category": "Services", "name": "JUPYTERHUB_SERVICE_GLANCES", "value": os.environ.get("JUPYTERHUB_SERVICE_GLANCES", "1"), "description": "Glances service (0=disabled, 1=enabled)"})
|
||||
settings.append({"category": "Services", "name": "JUPYTERHUB_SERVICE_TENSORBOARD", "value": os.environ.get("JUPYTERHUB_SERVICE_TENSORBOARD", "1"), "description": "TensorBoard service (0=disabled, 1=enabled)"})
|
||||
|
||||
# Idle Culler
|
||||
settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_ENABLED", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_ENABLED", "0"), "description": "Idle culler (0=disabled, 1=enabled)"})
|
||||
settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_TIMEOUT", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_TIMEOUT", "86400"), "description": "Idle timeout in seconds"})
|
||||
settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_INTERVAL", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_INTERVAL", "600"), "description": "Check interval in seconds"})
|
||||
settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_MAX_AGE", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_MAX_AGE", "0"), "description": "Max server age (0=unlimited)"})
|
||||
|
||||
# Branding
|
||||
settings.append({"category": "Branding", "name": "JUPYTERHUB_CUSTOM_LOGO_URI", "value": os.environ.get("JUPYTERHUB_CUSTOM_LOGO_URI", "") or "(default)", "description": "Custom logo URI"})
|
||||
settings.append({"category": "Branding", "name": "STELLARS_JUPYTERHUB_VERSION", "value": os.environ.get("STELLARS_JUPYTERHUB_VERSION", "unknown"), "description": "Platform version"})
|
||||
# Load settings from YAML dictionary
|
||||
settings = self._load_settings()
|
||||
|
||||
# Render the template
|
||||
html = self.render_template("settings.html", sync=True, user=current_user, settings=settings)
|
||||
self.finish(html)
|
||||
|
||||
def _load_settings(self):
|
||||
"""Load settings from YAML dictionary file and populate with env values"""
|
||||
import yaml
|
||||
|
||||
settings = []
|
||||
|
||||
try:
|
||||
with open(self.SETTINGS_DICT_PATH, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Categories are top-level keys in the YAML
|
||||
for category, items in config.items():
|
||||
# Skip comment lines (strings starting with #)
|
||||
if not isinstance(items, list):
|
||||
continue
|
||||
|
||||
for item in items:
|
||||
name = item.get('name', '')
|
||||
default = str(item.get('default', ''))
|
||||
value = os.environ.get(name, default)
|
||||
|
||||
# Handle empty_display for empty values
|
||||
if not value and 'empty_display' in item:
|
||||
value = item['empty_display']
|
||||
|
||||
settings.append({
|
||||
"category": category,
|
||||
"name": name,
|
||||
"value": value,
|
||||
"description": item.get('description', '')
|
||||
})
|
||||
|
||||
except FileNotFoundError:
|
||||
self.log.error(f"[Settings Page] Settings dictionary not found: {self.SETTINGS_DICT_PATH}")
|
||||
except Exception as e:
|
||||
self.log.error(f"[Settings Page] Error loading settings dictionary: {e}")
|
||||
|
||||
return settings
|
||||
|
||||
Reference in New Issue
Block a user