configurator is a framework and CLI tool to define, compose, and export application profiles (Code Server, GPU Code Server, Remote Desktop, …) into a single YAML configuration consumable by an Application Hub (e.g. JupyterHub + KubeSpawner).
The design focuses on:
- explicit configuration (no magic)
- strong typing (Pydantic)
- extensible base classes
- deterministic profile selection
- deployment-time overrides via CLI
A Profile represents a runnable application configuration:
- container image
- CPU / memory limits
- node selector
- volumes
- environment variables
- config maps
- init containers
- optional default URL
Profiles are serialized into a YAML file via the Config model.
BaseAppProfile is the foundation for all applications.
It implements:
- profile construction (
build()) - resource wiring
- config map handling
- init containers
- volume merging
- node selector handling
- default URL propagation
Concrete profiles never override build().
Profiles are grouped by base app classes:
BaseCoderProfile→ code / notebook / CWL appsBaseRemoteDesktopProfile→ web-based desktop appsBaseJupyterLabProfile→ JupyterLab apps
Each family defines sane defaults for:
- image
- resources
- volumes
- environment
Profiles are registered explicitly in a central registry.
- Each profile has a unique slug
- Deployments select profiles by slug
- No auto-discovery or hidden imports
This makes profile selection deterministic and debuggable.
configurator/
cli/
commands.py # Click command definitions
options.py # Shared CLI options
describe/
profile.py # Human-readable profile output
io/
yaml_writer.py # YAML serialization
overrides/
apply.py # Override application
parser.py # Override parsing
plugins/
loader.py # Builtin/external profile loading
apps/
base.py # BaseAppProfile
registry.py # ProfileRegistry
coder/
base_coder.py # BaseCoderProfile
coder_profiles.py # CoderProfile, GpuCoderProfile
__init__.py # re-exports + registration
remote_desktop/
base_remote_desktop.py # BaseRemoteDesktopProfile
remote_desktop_profiles.py
__init__.py # re-exports + registration
jupyterlab/
base_jupyterlab.py # BaseJupyterLabProfile
jupyterlab_profiles.py # JupyterLab profiles
models.py # Pydantic models
main.py # click-based CLI entrypoint
class BaseCoderProfile(BaseAppProfile):
image = "ghcr.io/eoepca/pde-code-server:latest-dev"
cpu_guarantee = 1
cpu_limit = 2
mem_guarantee = "4G"
mem_limit = "6G"class CoderProfile(BaseCoderProfile):
display_name = "Code Server"
description = "Code Server for development"
slug = "coder_app"class GpuCoderProfile(BaseCoderProfile):
display_name = "GPU Code Server"
description = "Code Server with GPU acceleration"
slug = "gpu_coder_app"
cpu_limit = 8
mem_limit = "32G"
def get_extra_resource_limits(self):
return {"nvidia.com/gpu": 1}class BaseRemoteDesktopProfile(BaseAppProfile):
image = "ghcr.io/eoepca/iga-remote-desktop:1.2.0"
default_url = "/desktop"Profiles are registered explicitly in their package init.py:
from configurator.apps import profile_registry
from .coder_profiles import CoderProfile, GpuCoderProfile, DaskGatewayCoderProfile
profile_registry.register(CoderProfile)
profile_registry.register(GpuCoderProfile)
profile_registry.register(DaskGatewayCoderProfile)Only concrete profiles are registered.
Base classes are never registered.
The CLI is implemented using Click.
dump-config --help
Option Description --profiles Comma-separated list of profile slugs --groups Comma-separated list of groups --node-selector Per-profile node selector override --override Per-profile attribute override --output Output YAML file
Environment variables can be used as defaults.
dump-config \
--profiles coder_app,gpu_coder_app,remote_desktop
Profiles are selected by slug, in order.
dump-config \
--profiles coder_app \
--groups developers,ml-users
Groups are deployment-level policy and apply to all selected profiles.
Syntax
--node-selector <slug>:<key>=<value>
Example
dump-config \
--profiles coder_app,gpu_coder_app \
--node-selector gpu_coder_app:nodepool=gpu
Result (excerpt)
profiles:
- id: profile_gpu_coder_app
node_selector:
nodepool: gpuSyntax
--override <slug>:<field>=<value>
Supported fields include:
cpu_limitcpu_guaranteemem_limitmem_guaranteeimage- any other profile attribute
dump-config \
--profiles coder_app \
--override coder_app:cpu_limit=4 \
--override coder_app:mem_limit=8G
dump-config \
--profiles coder_app \
--override coder_app:image=ghcr.io/eoepca/pde-code-server:2024.11
Combined Example
dump-config \
--profiles coder_app,gpu_coder_app,remote_desktop \
--groups developers,ml-users \
--node-selector gpu_coder_app:nodepool=gpu \
--override coder_app:cpu_limit=4 \
--override gpu_coder_app:mem_limit=32G \
--output config.yaml
The CLI generates a single YAML file:
profiles:
- id: profile_coder_app
groups:
- developers
- ml-users
definition:
display_name: Code Server
slug: coder_app
kubespawner_override:
cpu_limit: 4
mem_limit: 6G
image: ghcr.io/eoepca/pde-code-server:latest-devSerialization uses:
config.model_dump(exclude_none=True)
No Python-specific YAML tags are emitted.
- Profiles define defaults, not deployment policy
- Deployment overrides happen only via the CLI
- Selection is by slug, not by class name
- Concrete profiles never override build()
- Base classes define shared behavior
- CLI remains the single control plane
This design supports, without refactoring:
- new app families
- cluster-specific overrides
- GPU / non-GPU deployments
- multiple environments
- CI-generated configs
- Helm / GitOps workflows
dump-config \
--profiles coder_app,gpu_coder_app,remote_desktop,qgis_remote_desktop,jupyterlab_small \
--override coder_app:image=ghcr.io/eoepca/pde-code-server:2024.11 \
--groups group-a,group-b,group-c
dump-config \
--profiles coder_app,gpu_coder_app,remote_desktop,qgis_remote_desktop,jupyterlab_small \
--override coder_app:image=ghcr.io/eoepca/pde-code-server:2024.11 \
--groups group-a,group-b,group-c \
--override gpu_coder_app:groups=ml-users,gpu-users
dump-config \
--profiles-dir ./data/work/extra-profiles \
--profiles coder_app,training_how_to_app,mlflow_coder_app \
--groups group-a,group-b \
--output config.yaml