-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide an API endpoint to get current effective traitlets configuration #4406
Comments
Seems reasonable. Depending on how it's implemented, this probably won't get unconfigured default values, which come from classes and not the config object. Do you expect that to be an issue? |
Is there a way to get those too? Ideally I'd love to say this is the current effective configuration according to traitlets, and that should include the defaults right? Rather than just the overrides. |
It's definitely possible. Unfortunately, it's a bit tricky for Spawners in particular, because the 'right' way to get the current resolved configuration requires instantiating a configurable object, so you can let Configurable handle resolving dynamic defaults. But what Spawner do you instantiate to get that? It's not a problem for JupyterHub or Authenticator, where there's an instance already present. It's rather straightforward, given an instantiated configurable clsname = obj.__class__.__name__
for name in obj.traits(config=True):
value = getattr(obj, name)
print(f"{clsname}.{name} = {value!r}") For simple defaults, you can use from kubespawner import KubeSpawner
cls = KubeSpawner
for name, t in cls.class_traits(config=True).items():
print(f"{cls.__name__}.{name} = {t.default_value!r}") gives outputKubeSpawner.after_pod_created_hook = None
KubeSpawner.allow_privilege_escalation = False
KubeSpawner.args = traitlets.Undefined
KubeSpawner.auth_state_hook = None
KubeSpawner.automount_service_account_token = None
KubeSpawner.cmd = None
KubeSpawner.common_labels = traitlets.Undefined
KubeSpawner.component_label = 'singleuser-server'
KubeSpawner.consecutive_failure_limit = 0
KubeSpawner.container_security_context = traitlets.Undefined
KubeSpawner.cpu_guarantee = None
KubeSpawner.cpu_limit = None
KubeSpawner.debug = False
KubeSpawner.default_url = ''
KubeSpawner.delete_grace_period = 1
KubeSpawner.delete_pvc = True
KubeSpawner.delete_stopped_pods = True
KubeSpawner.disable_user_config = False
KubeSpawner.dns_name_template = '{name}.{namespace}.svc.cluster.local'
KubeSpawner.enable_user_namespaces = False
KubeSpawner.env_keep = traitlets.Undefined
KubeSpawner.environment = traitlets.Undefined
KubeSpawner.events_enabled = True
KubeSpawner.extra_annotations = traitlets.Undefined
KubeSpawner.extra_container_config = traitlets.Undefined
KubeSpawner.extra_containers = traitlets.Undefined
KubeSpawner.extra_labels = traitlets.Undefined
KubeSpawner.extra_pod_config = traitlets.Undefined
KubeSpawner.extra_resource_guarantees = traitlets.Undefined
KubeSpawner.extra_resource_limits = traitlets.Undefined
KubeSpawner.fs_gid = None
KubeSpawner.get_pod_url = None
KubeSpawner.gid = None
KubeSpawner.http_timeout = 30
KubeSpawner.hub_connect_ip = ''
KubeSpawner.hub_connect_port = 0
KubeSpawner.hub_connect_url = None
KubeSpawner.image = 'jupyterhub/singleuser:latest'
KubeSpawner.image_pull_policy = 'IfNotPresent'
KubeSpawner.image_pull_secrets = traitlets.Undefined
KubeSpawner.init_containers = traitlets.Undefined
KubeSpawner.ip = '0.0.0.0'
KubeSpawner.k8s_api_host = ''
KubeSpawner.k8s_api_request_retry_timeout = 30
KubeSpawner.k8s_api_request_timeout = 3
KubeSpawner.k8s_api_ssl_ca_cert = ''
KubeSpawner.k8s_api_threadpool_workers = 0
KubeSpawner.lifecycle_hooks = traitlets.Undefined
KubeSpawner.mem_guarantee = None
KubeSpawner.mem_limit = None
KubeSpawner.modify_pod_hook = None
KubeSpawner.namespace = ''
KubeSpawner.node_affinity_preferred = traitlets.Undefined
KubeSpawner.node_affinity_required = traitlets.Undefined
KubeSpawner.node_selector = traitlets.Undefined
KubeSpawner.notebook_dir = ''
KubeSpawner.oauth_client_allowed_scopes = traitlets.Undefined
KubeSpawner.oauth_roles = traitlets.Undefined
KubeSpawner.options_form = traitlets.Undefined
KubeSpawner.options_from_form = traitlets.Undefined
KubeSpawner.pod_affinity_preferred = traitlets.Undefined
KubeSpawner.pod_affinity_required = traitlets.Undefined
KubeSpawner.pod_anti_affinity_preferred = traitlets.Undefined
KubeSpawner.pod_anti_affinity_required = traitlets.Undefined
KubeSpawner.pod_connect_ip = ''
KubeSpawner.pod_name_template = 'jupyter-{username}--{servername}'
KubeSpawner.pod_security_context = traitlets.Undefined
KubeSpawner.poll_interval = 30
KubeSpawner.port = 0
KubeSpawner.post_stop_hook = None
KubeSpawner.pre_spawn_hook = None
KubeSpawner.priority_class_name = ''
KubeSpawner.privileged = False
KubeSpawner.profile_form_template = '\n <style>\n /*\n .profile divs holds two div tags: one for a radio button, and one\n for the profile\'s content.\n */\n #kubespawner-profiles-list .profile {\n display: flex;\n flex-direction: row;\n font-weight: normal;\n border-bottom: 1px solid #ccc;\n padding-bottom: 12px;\n }\n\n #kubespawner-profiles-list .profile .radio {\n padding: 12px;\n }\n\n /* .option divs holds a label and a select tag */\n #kubespawner-profiles-list .profile .option {\n display: flex;\n flex-direction: row;\n align-items: center;\n padding-bottom: 12px;\n }\n\n #kubespawner-profiles-list .profile .option label {\n font-weight: normal;\n margin-right: 8px;\n min-width: 96px;\n }\n </style>\n\n <div class=\'form-group\' id=\'kubespawner-profiles-list\'>\n {%- for profile in profile_list %}\n {#- Wrap everything in a <label> so clicking anywhere selects the option #}\n <label for=\'profile-item-{{ profile.slug }}\' class=\'profile\'>\n <div class=\'radio\'>\n <input type=\'radio\' name=\'profile\' id=\'profile-item-{{ profile.slug }}\' value=\'{{ profile.slug }}\' {% if profile.default %}checked{% endif %} />\n </div>\n <div>\n <h3>{{ profile.display_name }}</h3>\n\n {%- if profile.description %}\n <p>{{ profile.description }}</p>\n {%- endif %}\n\n {%- if profile.profile_options %}\n <div>\n {%- for k, option in profile.profile_options.items() %}\n <div class=\'option\'>\n <label for=\'profile-option-{{profile.slug}}-{{k}}\'>{{option.display_name}}</label>\n <select name="profile-option-{{profile.slug}}-{{k}}" class="form-control">\n {%- for k, choice in option[\'choices\'].items() %}\n <option value="{{ k }}" {% if choice.default %}selected{%endif %}>{{ choice.display_name }}</option>\n {%- endfor %}\n </select>\n </div>\n {%- endfor %}\n </div>\n {%- endif %}\n </div>\n </label>\n {%- endfor %}\n </div>\n '
KubeSpawner.profile_list = traitlets.Undefined
KubeSpawner.pvc_name_template = 'claim-{username}--{servername}'
KubeSpawner.scheduler_name = None
KubeSpawner.secret_mount_path = '/etc/jupyterhub/ssl/'
KubeSpawner.secret_name_template = 'jupyter-{username}{servername}'
KubeSpawner.server_token_scopes = traitlets.Undefined
KubeSpawner.service_account = None
KubeSpawner.services_enabled = False
KubeSpawner.ssl_alt_names = traitlets.Undefined
KubeSpawner.ssl_alt_names_include_local = True
KubeSpawner.start_timeout = 60
KubeSpawner.storage_access_modes = traitlets.Undefined
KubeSpawner.storage_capacity = None
KubeSpawner.storage_class = None
KubeSpawner.storage_extra_annotations = traitlets.Undefined
KubeSpawner.storage_extra_labels = traitlets.Undefined
KubeSpawner.storage_pvc_ensure = False
KubeSpawner.storage_selector = traitlets.Undefined
KubeSpawner.supplemental_gids = traitlets.Undefined
KubeSpawner.tolerations = traitlets.Undefined
KubeSpawner.uid = None
KubeSpawner.user_namespace_annotations = traitlets.Undefined
KubeSpawner.user_namespace_labels = traitlets.Undefined
KubeSpawner.user_namespace_template = '{hubnamespace}-{username}'
KubeSpawner.volume_mounts = traitlets.Undefined
KubeSpawner.volumes = traitlets.Undefined
KubeSpawner.working_dir = None If you don't instantiate the object, you'd also have to reimplement resolving which values are configured vs the default value. Not the most complex, but definitely nontrivial to handle everything from inheritance (Spawner and KubeSpawner both affect KubeSpawner) to LazyConfigValues The simplest way would definitely be to instantiate a fake Spawner with a mock User not hooked up to the db. You might hit edge cases in exactly how 'realistic' the mock is. The last challenge would be to locate additional configurable classes (e.g. if the Spawner delegates some of its config to sub-objects, as kubespawner does in |
I would say we can open by looking for traitlets' default_value, and being okay with |
Yeah, I think that's okay. FWIW, here's instantiating a Spawner with a mock user (could be more robust if it were built-in): from dataclasses import dataclass, field
from traitlets.config import Config
from jupyterhub.user import User
from kubespawner import KubeSpawner
@dataclass
class MockORMUser:
id: int = 0
name: str = "username"
admin: bool = False
encrypted_auth_state: bytes | None = None
state: dict = field(default_factory=dict)
# relationships likely to be used
orm_spawners: dict = field(default_factory=dict)
groups: list = field(default_factory=list)
# other fields technically exist, but shouldn't be accessed by Spawners
# these likely won't be exposed when we separate Spawners from db access
created = None
last_activity = None
cookie_id: str = ""
roles: list = field(default_factory=list)
# mock out _new_orm_spawner to avoid trying to perform a db operation
class MockUser(User):
def _new_orm_spawner(self, servername=""):
pass
class MockDB:
def add(self, obj):
pass
def commit(self):
pass
user = MockUser(orm_user=MockORMUser(), db=MockDB())
config = Config()
config.Spawner.cmd = "custom-command"
spawner = KubeSpawner(config=config, user=user)
obj = spawner
clsname = obj.__class__.__name__
for name in obj.traits(config=True):
value = getattr(obj, name)
print(f"{clsname}.{name} = {value!r}") which gives
|
@minrk think i can bribe you to do this somehow? :D |
This seems like a very useful feature. Is there any way to get the traitlets (however hacky) configuration in a service as of now? I understand I can set environment variable for the service, but not everything I need can be easily passed via environment variable, imagine a complex python object or a function/callable, etc. |
#4442 adds this and is awaiting review. But complex objects like Python functions are always going to be hard to pass around. Depending on your deployment, one option is to pass the config files themselves and actually load the config files directly, e.g. from jupyterhub.app import JupyterHub
hub = JupyterHub()
hub.load_config_file(hub.config_file) # or '/path/to/jupyterhub_config.py'
config = hub.config To get the full effect of config, it's always best to access attributes of a configured object instead of directly accessing a config object (e.g. |
When extending JupyterHub via services, it is quite difficult to figure out what exactly is the effective configuration of the hub. For example, there isn't a way to know what image is going to be launched, based on traitlets configuration! It would be extremely helpful to have an API endpoint that just dumps out traitlets values. This would drastically increase the ability of things like the jupyterhub configurator which currently can't tell what the default image is, or the resource limits, etc.
However, dumping all traitlets info is probably a bad idea, because it can contain sensitive info. So I propose we have an allow-list of traitlet values that will be exposed readonly via this config API. So it would be something like:
And then if you hit a
/api/config
endpoint, you'll get back the current calculated values of all these allowed traitlets.The text was updated successfully, but these errors were encountered: