allow spawners and authenticators to register via entrypoints

jupyterhub.authenticators for authenticators, jupyterhub.spawners for spawners

This has the effect that authenticators and spawners can be selected by name instead of full import string (e.g. 'github' or 'dummy' or 'kubernetes')
and, perhaps more importantly, the autogenerated configuration file will include a section for each installed and registered class.
This commit is contained in:
Min RK
2018-09-28 10:08:10 +02:00
parent 5c3530cc7f
commit c02ab23b3d
6 changed files with 164 additions and 20 deletions

View File

@@ -68,7 +68,6 @@ Writing an Authenticator that looks up passwords in a dictionary
requires only overriding this one method: requires only overriding this one method:
```python ```python
from tornado import gen
from IPython.utils.traitlets import Dict from IPython.utils.traitlets import Dict
from jupyterhub.auth import Authenticator from jupyterhub.auth import Authenticator
@@ -78,8 +77,7 @@ class DictionaryAuthenticator(Authenticator):
help="""dict of username:password for authentication""" help="""dict of username:password for authentication"""
) )
@gen.coroutine async def authenticate(self, handler, data):
def authenticate(self, handler, data):
if self.passwords.get(data['username']) == data['password']: if self.passwords.get(data['username']) == data['password']:
return data['username'] return data['username']
``` ```
@@ -136,6 +134,41 @@ See a list of custom Authenticators [on the wiki](https://github.com/jupyterhub/
If you are interested in writing a custom authenticator, you can read If you are interested in writing a custom authenticator, you can read
[this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html). [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html).
### Registering custom Authenticators via entry points
As of JupyterHub 1.0, custom authenticators can register themselves via
the `jupyterhub.authenticators` entry point metadata.
To do this, in your `setup.py` add:
```python
setup(
...
entry_points={
'jupyterhub.authenticators': [
'myservice = mypackage:MyAuthenticator',
],
},
)
```
If you have added this metadata to your package,
users can select your authenticator with the configuration:
```python
c.JupyterHub.authenticator_class = 'myservice'
```
instead of the full
```python
c.JupyterHub.authenticator_class = 'mypackage:MyAuthenticator'
```
previously required.
Additionally, configurable attributes for your spawner will
appear in jupyterhub help output and auto-generated configuration files
via `jupyterhub --generate-config`.
### Authentication state ### Authentication state

View File

@@ -10,6 +10,7 @@ and a custom Spawner needs to be able to take three actions:
## Examples ## Examples
Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners). Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
Some examples include: Some examples include:
@@ -174,6 +175,42 @@ When `Spawner.start` is called, this dictionary is accessible as `self.user_opti
If you are interested in building a custom spawner, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/spawners.html). If you are interested in building a custom spawner, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/spawners.html).
### Registering custom Spawners via entry points
As of JupyterHub 1.0, custom Spawners can register themselves via
the `jupyterhub.spawners` entry point metadata.
To do this, in your `setup.py` add:
```python
setup(
...
entry_points={
'jupyterhub.spawners': [
'myservice = mypackage:MySpawner',
],
},
)
```
If you have added this metadata to your package,
users can select your authenticator with the configuration:
```python
c.JupyterHub.spawner_class = 'myservice'
```
instead of the full
```python
c.JupyterHub.spawner_class = 'mypackage:MySpawner'
```
previously required.
Additionally, configurable attributes for your spawner will
appear in jupyterhub help output and auto-generated configuration files
via `jupyterhub --generate-config`.
## Spawners, resource limits, and guarantees (Optional) ## Spawners, resource limits, and guarantees (Optional)
Some spawners of the single-user notebook servers allow setting limits or Some spawners of the single-user notebook servers allow setting limits or

View File

@@ -42,7 +42,7 @@ from traitlets import (
Tuple, Type, Set, Instance, Bytes, Float, Tuple, Type, Set, Instance, Bytes, Float,
observe, default, observe, default,
) )
from traitlets.config import Application, catch_config_error from traitlets.config import Application, Configurable, catch_config_error
here = os.path.dirname(__file__) here = os.path.dirname(__file__)
@@ -58,7 +58,7 @@ from .oauth.provider import make_provider
from ._data import DATA_FILES_PATH from ._data import DATA_FILES_PATH
from .log import CoroutineLogFormatter, log_request from .log import CoroutineLogFormatter, log_request
from .proxy import Proxy, ConfigurableHTTPProxy from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix, Command from .traitlets import URLPrefix, Command, EntryPointType
from .utils import ( from .utils import (
maybe_future, maybe_future,
url_path_join, url_path_join,
@@ -227,13 +227,19 @@ class JupyterHub(Application):
'upgrade-db': (UpgradeDB, "Upgrade your JupyterHub state database to the current version."), 'upgrade-db': (UpgradeDB, "Upgrade your JupyterHub state database to the current version."),
} }
classes = List([ classes = List()
Spawner, @default('classes')
LocalProcessSpawner, def _load_classes(self):
Authenticator, classes = [Spawner, Authenticator, CryptKeeper]
PAMAuthenticator, for name, trait in self.traits(config=True).items():
CryptKeeper, # load entry point groups into configurable class list
]) # so that they show up in config files, etc.
if isinstance(trait, EntryPointType):
for key, entry_point in trait.load_entry_points().items():
cls = entry_point.load()
if cls not in classes and isinstance(cls, Configurable):
classes.append(cls)
return classes
load_groups = Dict(List(Unicode()), load_groups = Dict(List(Unicode()),
help="""Dict of 'group': ['usernames'] to load at startup. help="""Dict of 'group': ['usernames'] to load at startup.
@@ -651,20 +657,25 @@ class JupyterHub(Application):
).tag(config=True) ).tag(config=True)
_service_map = Dict() _service_map = Dict()
authenticator_class = Type(PAMAuthenticator, Authenticator, authenticator_class = EntryPointType(
default_value=PAMAuthenticator,
klass=Authenticator,
entry_point_group="jupyterhub.authenticators",
help="""Class for authenticating users. help="""Class for authenticating users.
This should be a class with the following form: This should be a subclass of :class:`jupyterhub.auth.Authenticator`
- constructor takes one kwarg: `config`, the IPython config object. with an :meth:`authenticate` method that:
with an authenticate method that:
- is a coroutine (asyncio or tornado) - is a coroutine (asyncio or tornado)
- returns username on success, None on failure - returns username on success, None on failure
- takes two arguments: (handler, data), - takes two arguments: (handler, data),
where `handler` is the calling web.RequestHandler, where `handler` is the calling web.RequestHandler,
and `data` is the POST form data from the login page. and `data` is the POST form data from the login page.
.. versionchanged:: 1.0
authenticators may be registered via entry points,
e.g. `c.JupyterHub.authenticator_class = 'pam'`
""" """
).tag(config=True) ).tag(config=True)
@@ -679,10 +690,17 @@ class JupyterHub(Application):
).tag(config=True) ).tag(config=True)
# class for spawning single-user servers # class for spawning single-user servers
spawner_class = Type(LocalProcessSpawner, Spawner, spawner_class = EntryPointType(
default_value=LocalProcessSpawner,
klass=Spawner,
entry_point_group="jupyterhub.spawners",
help="""The class to use for spawning single-user servers. help="""The class to use for spawning single-user servers.
Should be a subclass of Spawner. Should be a subclass of :class:`jupyterhub.spawner.Spawner`.
.. versionchanged:: 1.0
spawners may be registered via entry points,
e.g. `c.JupyterHub.spawner_class = 'localprocess'`
""" """
).tag(config=True) ).tag(config=True)

View File

@@ -4,7 +4,8 @@ Traitlets that are used in JupyterHub
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
from traitlets import List, Unicode, Integer, TraitType, TraitError import entrypoints
from traitlets import List, Unicode, Integer, Type, TraitType, TraitError
class URLPrefix(Unicode): class URLPrefix(Unicode):
@@ -91,3 +92,46 @@ class Callable(TraitType):
return value return value
else: else:
self.error(obj, value) self.error(obj, value)
class EntryPointType(Type):
"""Entry point-extended Type
classes can be registered via entry points
in addition to standard 'mypackage.MyClass' strings
"""
_original_help = ''
def __init__(self, *args, entry_point_group, **kwargs):
self.entry_point_group = entry_point_group
super().__init__(*args, **kwargs)
@property
def help(self):
"""Extend help by listing currently installed choices"""
chunks = [self._original_help]
chunks.append("Currently installed: ")
for key, entry_point in self.load_entry_points().items():
chunks.append(" - {}: {}.{}".format(key, entry_point.module_name, entry_point.object_name))
return '\n'.join(chunks)
@help.setter
def help(self, value):
self._original_help = value
def load_entry_points(self):
"""Load my entry point group"""
# load the group
group = entrypoints.get_group_named(self.entry_point_group)
# make it case-insensitive
return {key.lower(): value for key, value in group.items()}
def validate(self, obj, value):
if isinstance(value, str):
# first, look up in entry point registry
registry = self.load_entry_points()
key = value.lower()
if key in registry:
value = registry[key].load()
return super().validate(obj, value)

View File

@@ -1,5 +1,6 @@
alembic alembic
async_generator>=1.8 async_generator>=1.8
entrypoints
traitlets>=4.3.2 traitlets>=4.3.2
tornado>=5.0 tornado>=5.0
jinja2 jinja2

View File

@@ -106,6 +106,17 @@ setup_args = dict(
platforms = "Linux, Mac OS X", platforms = "Linux, Mac OS X",
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'], keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
python_requires = ">=3.5", python_requires = ">=3.5",
entry_points = {
'jupyterhub.authenticators': [
'default = jupyterhub.auth:PAMAuthenticator',
'pam = jupyterhub.auth:PAMAuthenticator',
'dummy = jupyterhub.auth:DummyAuthenticator',
],
'jupyterhub.spawners': [
'default = jupyterhub.spawner:LocalProcessSpawner',
'localprocess = jupyterhub.spawner:LocalProcessSpawner',
],
},
classifiers = [ classifiers = [
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',