Skip to content
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

allow spawners and authenticators to register via entry points #2203

Merged
merged 1 commit into from Oct 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 36 additions & 3 deletions docs/source/reference/authenticators.md
Expand Up @@ -68,7 +68,6 @@ Writing an Authenticator that looks up passwords in a dictionary
requires only overriding this one method:

```python
from tornado import gen
from IPython.utils.traitlets import Dict
from jupyterhub.auth import Authenticator

Expand All @@ -78,8 +77,7 @@ class DictionaryAuthenticator(Authenticator):
help="""dict of username:password for authentication"""
)

@gen.coroutine
def authenticate(self, handler, data):
async def authenticate(self, handler, data):
if self.passwords.get(data['username']) == data['password']:
return data['username']
```
Expand Down Expand Up @@ -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
[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

Expand Down
37 changes: 37 additions & 0 deletions docs/source/reference/spawners.md
Expand Up @@ -10,6 +10,7 @@ and a custom Spawner needs to be able to take three actions:


## Examples

Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
Some examples include:

Expand Down Expand Up @@ -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).

### 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)

Some spawners of the single-user notebook servers allow setting limits or
Expand Down
50 changes: 34 additions & 16 deletions jupyterhub/app.py
Expand Up @@ -42,7 +42,7 @@
Tuple, Type, Set, Instance, Bytes, Float,
observe, default,
)
from traitlets.config import Application, catch_config_error
from traitlets.config import Application, Configurable, catch_config_error

here = os.path.dirname(__file__)

Expand All @@ -58,7 +58,7 @@
from ._data import DATA_FILES_PATH
from .log import CoroutineLogFormatter, log_request
from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix, Command
from .traitlets import URLPrefix, Command, EntryPointType
from .utils import (
maybe_future,
url_path_join,
Expand Down Expand Up @@ -227,13 +227,19 @@ class JupyterHub(Application):
'upgrade-db': (UpgradeDB, "Upgrade your JupyterHub state database to the current version."),
}

classes = List([
Spawner,
LocalProcessSpawner,
Authenticator,
PAMAuthenticator,
CryptKeeper,
])
classes = List()
@default('classes')
def _load_classes(self):
classes = [Spawner, Authenticator, CryptKeeper]
for name, trait in self.traits(config=True).items():
# 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()),
help="""Dict of 'group': ['usernames'] to load at startup.
Expand Down Expand Up @@ -651,20 +657,25 @@ def _deprecate_api_tokens(self, change):
).tag(config=True)
_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.

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 authenticate method that:
with an :meth:`authenticate` method that:

- is a coroutine (asyncio or tornado)
- returns username on success, None on failure
- takes two arguments: (handler, data),
where `handler` is the calling web.RequestHandler,
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)

Expand All @@ -679,10 +690,17 @@ def _authenticator_default(self):
).tag(config=True)

# 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.

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)

Expand Down
46 changes: 45 additions & 1 deletion jupyterhub/traitlets.py
Expand Up @@ -4,7 +4,8 @@
# Copyright (c) Jupyter Development Team.
# 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):
Expand Down Expand Up @@ -91,3 +92,46 @@ def validate(self, obj, value):
return value
else:
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)
1 change: 1 addition & 0 deletions requirements.txt
@@ -1,5 +1,6 @@
alembic
async_generator>=1.8
entrypoints
traitlets>=4.3.2
tornado>=5.0
jinja2
Expand Down
11 changes: 11 additions & 0 deletions setup.py
Expand Up @@ -106,6 +106,17 @@ def get_package_data():
platforms = "Linux, Mac OS X",
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
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 = [
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
Expand Down