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:
stellarshenson
2026-01-14 16:43:26 +01:00
parent 022e970dbf
commit d8a306a0d5
4 changed files with 137 additions and 37 deletions

View File

@@ -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/

View 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

View File

@@ -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

View File

@@ -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