mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b186bdbce3 | ||
![]() |
36fe6c6f66 | ||
![]() |
8bf559db52 | ||
![]() |
750085f627 | ||
![]() |
2dc2c99b4a | ||
![]() |
e703555888 | ||
![]() |
7e102f0511 | ||
![]() |
facde96425 | ||
![]() |
608c746a59 | ||
![]() |
a8c834410f | ||
![]() |
bda14b487a | ||
![]() |
fd5cf8c360 | ||
![]() |
03758e5b46 | ||
![]() |
e540d143bb | ||
![]() |
b2c5ad40c5 | ||
![]() |
edfdf672d8 | ||
![]() |
39f19aef49 | ||
![]() |
8813bb63d4 | ||
![]() |
7c18d6fe14 | ||
![]() |
d1fe17d3cb | ||
![]() |
b8965c2017 | ||
![]() |
733d7bc158 | ||
![]() |
88f31c29bb | ||
![]() |
3caf3cfda8 | ||
![]() |
d076c55cca | ||
![]() |
3e185022c8 | ||
![]() |
857ee2885f | ||
![]() |
cd8dd56213 | ||
![]() |
f06902aa8f | ||
![]() |
bb109c6f75 | ||
![]() |
e525ec7b5b | ||
![]() |
356b98e19f | ||
![]() |
8c803e7a53 | ||
![]() |
2e21a6f4e0 | ||
![]() |
cfd31b14e3 | ||
![]() |
f03a620424 | ||
![]() |
440ad77ad5 |
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@@ -129,7 +129,7 @@ jobs:
|
|||||||
# If GITHUB_TOKEN isn't available (e.g. in PRs) returns no tags [].
|
# If GITHUB_TOKEN isn't available (e.g. in PRs) returns no tags [].
|
||||||
- name: Get list of jupyterhub tags
|
- name: Get list of jupyterhub tags
|
||||||
id: jupyterhubtags
|
id: jupyterhubtags
|
||||||
uses: jupyterhub/action-major-minor-tag-calculator@v1
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
with:
|
with:
|
||||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub:"
|
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub:"
|
||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get list of jupyterhub-onbuild tags
|
- name: Get list of jupyterhub-onbuild tags
|
||||||
id: onbuildtags
|
id: onbuildtags
|
||||||
uses: jupyterhub/action-major-minor-tag-calculator@v1
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
with:
|
with:
|
||||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-onbuild:"
|
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-onbuild:"
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get list of jupyterhub-demo tags
|
- name: Get list of jupyterhub-demo tags
|
||||||
id: demotags
|
id: demotags
|
||||||
uses: jupyterhub/action-major-minor-tag-calculator@v1
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
with:
|
with:
|
||||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-demo:"
|
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-demo:"
|
||||||
@@ -190,3 +190,23 @@ jobs:
|
|||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
|
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
|
||||||
|
|
||||||
|
# jupyterhub/singleuser
|
||||||
|
- name: Get list of jupyterhub/singleuser tags
|
||||||
|
id: singleusertags
|
||||||
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
|
with:
|
||||||
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
prefix: "${{ env.REGISTRY }}jupyterhub/singleuser:"
|
||||||
|
defaultTag: "${{ env.REGISTRY }}jupyterhub/singleuser:noref"
|
||||||
|
branchRegex: ^\w[\w-.]*$
|
||||||
|
|
||||||
|
- name: Build and push jupyterhub/singleuser
|
||||||
|
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }}
|
||||||
|
context: singleuser
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ join(fromJson(steps.singleusertags.outputs.tags)) }}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.29.0
|
rev: v2.29.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args:
|
args:
|
||||||
@@ -10,7 +10,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 21.9b0
|
rev: 21.11b1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
BIN
docs/source/images/binder-404.png
Normal file
BIN
docs/source/images/binder-404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
BIN
docs/source/images/binderhub-form.png
Normal file
BIN
docs/source/images/binderhub-form.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
BIN
docs/source/images/chp-404.png
Normal file
BIN
docs/source/images/chp-404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
BIN
docs/source/images/server-not-running.png
Normal file
BIN
docs/source/images/server-not-running.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@@ -114,7 +114,9 @@ class ScopeTableGenerator:
|
|||||||
if doc_description:
|
if doc_description:
|
||||||
description = doc_description
|
description = doc_description
|
||||||
scope_dict[scope] = description
|
scope_dict[scope] = description
|
||||||
content['securityDefinitions']['oauth2']['scopes'] = scope_dict
|
content['components']['securitySchemes']['oauth2']['flows'][
|
||||||
|
'authorizationCode'
|
||||||
|
]['scopes'] = scope_dict
|
||||||
|
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
yaml.dump(content, f)
|
yaml.dump(content, f)
|
||||||
|
128
docs/source/reference/api-only.md
Normal file
128
docs/source/reference/api-only.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
(api-only)=
|
||||||
|
|
||||||
|
# Deploying JupyterHub in "API only mode"
|
||||||
|
|
||||||
|
As a service for deploying and managing Jupyter servers for users, JupyterHub
|
||||||
|
exposes this functionality _primarily_ via a [REST API](rest).
|
||||||
|
For convenience, JupyterHub also ships with a _basic_ web UI built using that REST API.
|
||||||
|
The basic web UI enables users to click a button to quickly start and stop their servers,
|
||||||
|
and it lets admins perform some basic user and server management tasks.
|
||||||
|
|
||||||
|
The REST API has always provided additional functionality beyond what is available in the basic web UI.
|
||||||
|
Similarly, we avoid implementing UI functionality that is also not available via the API.
|
||||||
|
With JupyterHub 2.0, the basic web UI will **always** be composed using the REST API.
|
||||||
|
In other words, no UI pages should rely on information not available via the REST API.
|
||||||
|
Previously, some admin UI functionality could only be achieved via admin pages,
|
||||||
|
such as paginated requests.
|
||||||
|
|
||||||
|
## Limited UI customization via templates
|
||||||
|
|
||||||
|
The JupyterHub UI is customizable via extensible HTML [templates](templates),
|
||||||
|
but this has some limited scope to what can be customized.
|
||||||
|
Adding some content and messages to existing pages is well supported,
|
||||||
|
but changing the page flow and what pages are available are beyond the scope of what is customizable.
|
||||||
|
|
||||||
|
## Rich UI customization with REST API based apps
|
||||||
|
|
||||||
|
Increasingly, JupyterHub is used purely as an API for managing Jupyter servers
|
||||||
|
for other Jupyter-based applications that might want to present a different user experience.
|
||||||
|
If you want a fully customized user experience,
|
||||||
|
you can now disable the Hub UI and use your own pages together with the JupyterHub REST API
|
||||||
|
to build your own web application to serve your users,
|
||||||
|
relying on the Hub only as an API for managing users and servers.
|
||||||
|
|
||||||
|
One example of such an application is [BinderHub][], which powers https://mybinder.org,
|
||||||
|
and motivates many of these changes.
|
||||||
|
|
||||||
|
BinderHub is distinct from a traditional JupyterHub deployment
|
||||||
|
because it uses temporary users created for each launch.
|
||||||
|
Instead of presenting a login page,
|
||||||
|
users are presented with a form to specify what environment they would like to launch:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When a launch is requested:
|
||||||
|
|
||||||
|
1. an image is built, if necessary
|
||||||
|
2. a temporary user is created,
|
||||||
|
3. a server is launched for that user, and
|
||||||
|
4. when running, users are redirected to an already running server with an auth token in the URL
|
||||||
|
5. after the session is over, the user is deleted
|
||||||
|
|
||||||
|
This means that a lot of JupyterHub's UI flow doesn't make sense:
|
||||||
|
|
||||||
|
- there is no way for users to login
|
||||||
|
- the human user doesn't map onto a JupyterHub `User` in a meaningful way
|
||||||
|
- when a server isn't running, there isn't a 'restart your server' action available because the user has been deleted
|
||||||
|
- users do not have any access to any Hub functionality, so presenting pages for those features would be confusing
|
||||||
|
|
||||||
|
BinderHub is one of the motivating use cases for JupyterHub supporting being used _only_ via its API.
|
||||||
|
We'll use BinderHub here as an example of various configuration options.
|
||||||
|
|
||||||
|
[binderhub]: https://binderhub.readthedocs.io
|
||||||
|
|
||||||
|
## Disabling Hub UI
|
||||||
|
|
||||||
|
`c.JupyterHub.hub_routespec` is a configuration option to specify which URL prefix should be routed to the Hub.
|
||||||
|
The default is `/` which means that the Hub will receive all requests not already specified to be routed somewhere else.
|
||||||
|
|
||||||
|
There are three values that are most logical for `hub_routespec`:
|
||||||
|
|
||||||
|
- `/` - this is the default, and used in most deployments.
|
||||||
|
It is also the only option prior to JupyterHub 1.4.
|
||||||
|
- `/hub/` - this serves only Hub pages, both UI and API
|
||||||
|
- `/hub/api` - this serves _only the Hub API_, so all Hub UI is disabled,
|
||||||
|
aside from the OAuth confirmation page, if used.
|
||||||
|
|
||||||
|
If you choose a hub routespec other than `/`,
|
||||||
|
the main JupyterHub feature you will lose is the automatic handling of requests for `/user/:username`
|
||||||
|
when the requested server is not running.
|
||||||
|
|
||||||
|
JupyterHub's handling of this request shows this page,
|
||||||
|
telling you that the server is not running,
|
||||||
|
with a button to launch it again:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If you set `hub_routespec` to something other than `/`,
|
||||||
|
it is likely that you also want to register another destination for `/` to handle requests to not-running servers.
|
||||||
|
If you don't, you will see a default 404 page from the proxy:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For mybinder.org, the default "start my server" page doesn't make sense,
|
||||||
|
because when a server is gone, there is no restart action.
|
||||||
|
Instead, we provide hints about how to get back to a link to start a _new_ server:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To achieve this, mybinder.org registers a route for `/` that goes to a custom endpoint
|
||||||
|
that runs nginx and only serves this static HTML error page.
|
||||||
|
This is set with
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Proxy.extra_routes = {
|
||||||
|
"/": "http://custom-404-entpoint/",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may want to use an alternate behavior, such as redirecting to a landing page,
|
||||||
|
or taking some other action based on the requested page.
|
||||||
|
|
||||||
|
If you use `c.JupyterHub.hub_routespec = "/hub/"`,
|
||||||
|
then all the Hub pages will be available,
|
||||||
|
and only this default-page-404 issue will come up.
|
||||||
|
|
||||||
|
If you use `c.JupyterHub.hub_routespec = "/hub/api/"`,
|
||||||
|
then only the Hub _API_ will be available,
|
||||||
|
and all UI will be up to you.
|
||||||
|
mybinder.org takes this last option,
|
||||||
|
because none of the Hub UI pages really make sense.
|
||||||
|
Binder users don't have any reason to know or care that JupyterHub happens
|
||||||
|
to be an implementation detail of how their environment is managed.
|
||||||
|
Seeing Hub error pages and messages in that situation is more likely to be confusing than helpful.
|
||||||
|
|
||||||
|
:::{versionadded} 1.4
|
||||||
|
|
||||||
|
`c.JupyterHub.hub_routespec` and `c.Proxy.extra_routes` are new in JupyterHub 1.4.
|
||||||
|
:::
|
@@ -21,6 +21,7 @@ what happens under-the-hood when you deploy and configure your JupyterHub.
|
|||||||
monitoring
|
monitoring
|
||||||
database
|
database
|
||||||
templates
|
templates
|
||||||
|
api-only
|
||||||
../events/index
|
../events/index
|
||||||
config-user-env
|
config-user-env
|
||||||
config-examples
|
config-examples
|
||||||
|
@@ -5,7 +5,7 @@ Below is an interactive view of JupyterHub's OpenAPI specification.
|
|||||||
<!-- client-rendered openapi UI copied from FastAPI -->
|
<!-- client-rendered openapi UI copied from FastAPI -->
|
||||||
|
|
||||||
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
|
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4.1/swagger-ui-bundle.js"></script>
|
||||||
<!-- `SwaggerUIBundle` is now available on the page -->
|
<!-- `SwaggerUIBundle` is now available on the page -->
|
||||||
|
|
||||||
<!-- render the ui here -->
|
<!-- render the ui here -->
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
(rest-api)=
|
||||||
|
|
||||||
# Using JupyterHub's REST API
|
# Using JupyterHub's REST API
|
||||||
|
|
||||||
This section will give you information on:
|
This section will give you information on:
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
# 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.
|
||||||
# version_info updated by running `tbump`
|
# version_info updated by running `tbump`
|
||||||
version_info = (2, 0, 0, "rc1", "")
|
version_info = (2, 0, 0, "rc5", "")
|
||||||
|
|
||||||
# pep 440 version: no dot before beta/rc, but before .dev
|
# pep 440 version: no dot before beta/rc, but before .dev
|
||||||
# 0.1.0rc1
|
# 0.1.0rc1
|
||||||
|
@@ -31,6 +31,9 @@ class APIHandler(BaseHandler):
|
|||||||
- methods for REST API models
|
- methods for REST API models
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# accept token-based authentication for API requests
|
||||||
|
_accept_token_auth = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_security_policy(self):
|
def content_security_policy(self):
|
||||||
return '; '.join([super().content_security_policy, "default-src 'none'"])
|
return '; '.join([super().content_security_policy, "default-src 'none'"])
|
||||||
@@ -210,6 +213,7 @@ class APIHandler(BaseHandler):
|
|||||||
'last_activity': isoformat(token.last_activity),
|
'last_activity': isoformat(token.last_activity),
|
||||||
'expires_at': isoformat(token.expires_at),
|
'expires_at': isoformat(token.expires_at),
|
||||||
'note': token.note,
|
'note': token.note,
|
||||||
|
'session_id': token.session_id,
|
||||||
'oauth_client': token.oauth_client.description
|
'oauth_client': token.oauth_client.description
|
||||||
or token.oauth_client.identifier,
|
or token.oauth_client.identifier,
|
||||||
}
|
}
|
||||||
|
@@ -58,6 +58,14 @@ class SelfAPIHandler(APIHandler):
|
|||||||
|
|
||||||
model = get_model(user)
|
model = get_model(user)
|
||||||
|
|
||||||
|
# add session_id associated with token
|
||||||
|
# added in 2.0
|
||||||
|
token = self.get_token()
|
||||||
|
if token:
|
||||||
|
model["session_id"] = token.session_id
|
||||||
|
else:
|
||||||
|
model["session_id"] = None
|
||||||
|
|
||||||
# add scopes to identify model,
|
# add scopes to identify model,
|
||||||
# but not the scopes we added to ensure we could read our own model
|
# but not the scopes we added to ensure we could read our own model
|
||||||
model["scopes"] = sorted(self.expanded_scopes.difference(_added_scopes))
|
model["scopes"] = sorted(self.expanded_scopes.difference(_added_scopes))
|
||||||
|
@@ -71,6 +71,12 @@ SESSION_COOKIE_NAME = 'jupyterhub-session-id'
|
|||||||
class BaseHandler(RequestHandler):
|
class BaseHandler(RequestHandler):
|
||||||
"""Base Handler class with access to common methods and properties."""
|
"""Base Handler class with access to common methods and properties."""
|
||||||
|
|
||||||
|
# by default, only accept cookie-based authentication
|
||||||
|
# The APIHandler base class enables token auth
|
||||||
|
# versionadded: 2.0
|
||||||
|
_accept_cookie_auth = True
|
||||||
|
_accept_token_auth = False
|
||||||
|
|
||||||
async def prepare(self):
|
async def prepare(self):
|
||||||
"""Identify the user during the prepare stage of each request
|
"""Identify the user during the prepare stage of each request
|
||||||
|
|
||||||
@@ -340,6 +346,7 @@ class BaseHandler(RequestHandler):
|
|||||||
auth_info['auth_state'] = await user.get_auth_state()
|
auth_info['auth_state'] = await user.get_auth_state()
|
||||||
return await self.auth_to_user(auth_info, user)
|
return await self.auth_to_user(auth_info, user)
|
||||||
|
|
||||||
|
@functools.lru_cache()
|
||||||
def get_token(self):
|
def get_token(self):
|
||||||
"""get token from authorization header"""
|
"""get token from authorization header"""
|
||||||
token = self.get_auth_token()
|
token = self.get_auth_token()
|
||||||
@@ -410,9 +417,11 @@ class BaseHandler(RequestHandler):
|
|||||||
async def get_current_user(self):
|
async def get_current_user(self):
|
||||||
"""get current username"""
|
"""get current username"""
|
||||||
if not hasattr(self, '_jupyterhub_user'):
|
if not hasattr(self, '_jupyterhub_user'):
|
||||||
|
user = None
|
||||||
try:
|
try:
|
||||||
|
if self._accept_token_auth:
|
||||||
user = self.get_current_user_token()
|
user = self.get_current_user_token()
|
||||||
if user is None:
|
if user is None and self._accept_cookie_auth:
|
||||||
user = self.get_current_user_cookie()
|
user = self.get_current_user_cookie()
|
||||||
if user and isinstance(user, User):
|
if user and isinstance(user, User):
|
||||||
user = await self.refresh_auth(user)
|
user = await self.refresh_auth(user)
|
||||||
|
@@ -295,7 +295,7 @@ def get_scopes_for(orm_object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(orm_object, orm.APIToken):
|
if isinstance(orm_object, orm.APIToken):
|
||||||
app_log.warning(f"Authenticated with token {orm_object}")
|
app_log.debug(f"Authenticated with token {orm_object}")
|
||||||
owner = orm_object.user or orm_object.service
|
owner = orm_object.user or orm_object.service
|
||||||
token_scopes = roles.expand_roles_to_scopes(orm_object)
|
token_scopes = roles.expand_roles_to_scopes(orm_object)
|
||||||
if orm_object.client_id != "jupyterhub":
|
if orm_object.client_id != "jupyterhub":
|
||||||
|
@@ -1023,8 +1023,8 @@ class HubAuthenticated:
|
|||||||
self._hub_auth_user_cache = None
|
self._hub_auth_user_cache = None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# store tokens passed via url or header in a cookie for future requests
|
# store ?token=... tokens passed via url in a cookie for future requests
|
||||||
url_token = self.hub_auth.get_token(self)
|
url_token = self.get_argument('token', '')
|
||||||
if (
|
if (
|
||||||
user_model
|
user_model
|
||||||
and url_token
|
and url_token
|
||||||
|
@@ -18,6 +18,7 @@ import sys
|
|||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
from importlib import import_module
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -606,10 +607,34 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
t = self.hub_activity_interval * (1 + 0.2 * (random.random() - 0.5))
|
t = self.hub_activity_interval * (1 + 0.2 * (random.random() - 0.5))
|
||||||
await asyncio.sleep(t)
|
await asyncio.sleep(t)
|
||||||
|
|
||||||
|
def _log_app_versions(self):
|
||||||
|
"""Log application versions at startup
|
||||||
|
|
||||||
|
Logs versions of jupyterhub and singleuser-server base versions (jupyterlab, jupyter_server, notebook)
|
||||||
|
"""
|
||||||
|
self.log.info(f"Starting jupyterhub single-user server version {__version__}")
|
||||||
|
|
||||||
|
# don't log these package versions
|
||||||
|
seen = {"jupyterhub", "traitlets", "jupyter_core", "builtins"}
|
||||||
|
|
||||||
|
for cls in self.__class__.mro():
|
||||||
|
module_name = cls.__module__.partition(".")[0]
|
||||||
|
if module_name not in seen:
|
||||||
|
seen.add(module_name)
|
||||||
|
try:
|
||||||
|
mod = import_module(module_name)
|
||||||
|
mod_version = getattr(mod, "__version__")
|
||||||
|
except Exception:
|
||||||
|
mod_version = ""
|
||||||
|
self.log.info(
|
||||||
|
f"Extending {cls.__module__}.{cls.__name__} from {module_name} {mod_version}"
|
||||||
|
)
|
||||||
|
|
||||||
def initialize(self, argv=None):
|
def initialize(self, argv=None):
|
||||||
# disable trash by default
|
# disable trash by default
|
||||||
# this can be re-enabled by config
|
# this can be re-enabled by config
|
||||||
self.config.FileContentsManager.delete_to_trash = False
|
self.config.FileContentsManager.delete_to_trash = False
|
||||||
|
self._log_app_versions()
|
||||||
return super().initialize(argv)
|
return super().initialize(argv)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -715,6 +740,18 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
orig_loader = env.loader
|
orig_loader = env.loader
|
||||||
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
|
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
|
||||||
|
|
||||||
|
def load_server_extensions(self):
|
||||||
|
# Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect
|
||||||
|
r = super().load_server_extensions()
|
||||||
|
# clear the token in PageConfig at this step
|
||||||
|
# so that cookie auth is used
|
||||||
|
# FIXME: in the future,
|
||||||
|
# it would probably make sense to set page_config.token to the token
|
||||||
|
# from the current request.
|
||||||
|
if 'page_config_data' in self.web_app.settings:
|
||||||
|
self.web_app.settings['page_config_data']['token'] = ''
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def detect_base_package(App):
|
def detect_base_package(App):
|
||||||
"""Detect the base package for an App class
|
"""Detect the base package for an App class
|
||||||
|
@@ -578,6 +578,41 @@ async def test_login_page(app, url, params, redirected_url, form_action):
|
|||||||
assert action.endswith(form_action)
|
assert action.endswith(form_action)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url, token_in",
|
||||||
|
[
|
||||||
|
("/home", "url"),
|
||||||
|
("/home", "header"),
|
||||||
|
("/login", "url"),
|
||||||
|
("/login", "header"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_page_with_token(app, user, url, token_in):
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
token = user.new_api_token()
|
||||||
|
if token_in == "url":
|
||||||
|
url = url_concat(url, {"token": token})
|
||||||
|
headers = None
|
||||||
|
elif token_in == "header":
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# request a page with ?token= in URL shouldn't be allowed
|
||||||
|
r = await get_page(
|
||||||
|
url,
|
||||||
|
app,
|
||||||
|
headers=headers,
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
if "/hub/login" in r.url:
|
||||||
|
assert r.status_code == 200
|
||||||
|
else:
|
||||||
|
assert r.status_code == 302
|
||||||
|
assert r.headers["location"].partition("?")[0].endswith("/hub/login")
|
||||||
|
assert not r.cookies
|
||||||
|
|
||||||
|
|
||||||
async def test_login_fail(app):
|
async def test_login_fail(app):
|
||||||
name = 'wash'
|
name = 'wash'
|
||||||
base_url = public_url(app)
|
base_url = public_url(app)
|
||||||
|
@@ -1332,3 +1332,19 @@ async def test_token_keep_roles_on_restart():
|
|||||||
for token in user.api_tokens:
|
for token in user.api_tokens:
|
||||||
hub.db.delete(token)
|
hub.db.delete(token)
|
||||||
hub.db.commit()
|
hub.db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_login_default_role(app, username):
|
||||||
|
cookies = await app.login_user(username)
|
||||||
|
user = app.users[username]
|
||||||
|
# assert login new user gets 'user' role
|
||||||
|
assert [role.name for role in user.roles] == ["user"]
|
||||||
|
|
||||||
|
# clear roles, keep user
|
||||||
|
user.roles = []
|
||||||
|
app.db.commit()
|
||||||
|
|
||||||
|
# login *again*; user exists, shouldn't trigger change in roles
|
||||||
|
cookies = await app.login_user(username)
|
||||||
|
user = app.users[username]
|
||||||
|
assert user.roles == []
|
||||||
|
@@ -11,7 +11,7 @@ target_version = [
|
|||||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||||
|
|
||||||
[tool.tbump.version]
|
[tool.tbump.version]
|
||||||
current = "2.0.0rc1"
|
current = "2.0.0rc5"
|
||||||
|
|
||||||
# Example of a semver regexp.
|
# Example of a semver regexp.
|
||||||
# Make sure this matches current_version before
|
# Make sure this matches current_version before
|
||||||
|
5
setup.py
5
setup.py
@@ -46,10 +46,9 @@ def get_data_files():
|
|||||||
"""Get data files in share/jupyter"""
|
"""Get data files in share/jupyter"""
|
||||||
|
|
||||||
data_files = []
|
data_files = []
|
||||||
ntrim = len(here + os.path.sep)
|
|
||||||
|
|
||||||
for (d, dirs, filenames) in os.walk(share_jupyterhub):
|
for (d, dirs, filenames) in os.walk(share_jupyterhub):
|
||||||
data_files.append((d[ntrim:], [pjoin(d, f) for f in filenames]))
|
rel_d = os.path.relpath(d, here)
|
||||||
|
data_files.append((rel_d, [os.path.join(rel_d, f) for f in filenames]))
|
||||||
return data_files
|
return data_files
|
||||||
|
|
||||||
|
|
||||||
|
@@ -6,7 +6,6 @@ FROM $BASE_IMAGE
|
|||||||
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
||||||
|
|
||||||
ADD install_jupyterhub /tmp/install_jupyterhub
|
ADD install_jupyterhub /tmp/install_jupyterhub
|
||||||
ARG JUPYTERHUB_VERSION=main
|
ARG JUPYTERHUB_VERSION=git:HEAD
|
||||||
# install pinned jupyterhub and ensure jupyterlab is installed
|
# install pinned jupyterhub
|
||||||
RUN python3 /tmp/install_jupyterhub && \
|
RUN python3 /tmp/install_jupyterhub
|
||||||
python3 -m pip install jupyterlab
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
docker build --build-arg JUPYTERHUB_VERSION=$DOCKER_TAG -t $DOCKER_REPO:$DOCKER_TAG .
|
|
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
function get_hub_version() {
|
|
||||||
rm -f hub_version
|
|
||||||
V=$1
|
|
||||||
docker run --rm -v $PWD:/version -u $(id -u) -i $DOCKER_REPO:$DOCKER_TAG sh -c 'jupyterhub --version > /version/hub_version'
|
|
||||||
hub_xyz=$(cat hub_version)
|
|
||||||
split=( ${hub_xyz//./ } )
|
|
||||||
hub_xy="${split[0]}.${split[1]}"
|
|
||||||
# add .dev on hub_xy so it's 1.0.dev
|
|
||||||
if [[ ! -z "${split[3]:-}" ]]; then
|
|
||||||
hub_xy="${hub_xy}.${split[3]}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
# tag e.g. 0.9 with main
|
|
||||||
get_hub_version
|
|
||||||
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xy
|
|
||||||
docker push $DOCKER_REPO:$hub_xy
|
|
||||||
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xyz
|
|
||||||
docker push $DOCKER_REPO:$hub_xyz
|
|
@@ -3,19 +3,22 @@ import os
|
|||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
V = os.environ['JUPYTERHUB_VERSION']
|
version = os.environ['JUPYTERHUB_VERSION']
|
||||||
|
|
||||||
pip_install = [
|
pip_install = [
|
||||||
sys.executable, '-m', 'pip', 'install', '--no-cache', '--upgrade',
|
sys.executable,
|
||||||
'--upgrade-strategy', 'only-if-needed',
|
'-m',
|
||||||
|
'pip',
|
||||||
|
'install',
|
||||||
|
'--no-cache',
|
||||||
|
'--upgrade',
|
||||||
|
'--upgrade-strategy',
|
||||||
|
'only-if-needed',
|
||||||
]
|
]
|
||||||
if V in {'main', 'HEAD'}:
|
if version.startswith("git:"):
|
||||||
req = 'https://github.com/jupyterhub/jupyterhub/archive/HEAD.tar.gz'
|
ref = version.partition(":")[-1]
|
||||||
|
req = f"https://github.com/jupyterhub/jupyterhub/archive/{ref}.tar.gz"
|
||||||
else:
|
else:
|
||||||
version_info = [ int(part) for part in V.split('.') ]
|
req = f"jupyterhub=={version}"
|
||||||
version_info[-1] += 1
|
|
||||||
upper_bound = '.'.join(map(str, version_info))
|
|
||||||
vs = '>=%s,<%s' % (V, upper_bound)
|
|
||||||
req = 'jupyterhub%s' % vs
|
|
||||||
|
|
||||||
check_call(pip_install + [req])
|
check_call(pip_install + [req])
|
||||||
|
Reference in New Issue
Block a user