sync with master

This commit is contained in:
Min RK
2021-04-22 14:14:02 +02:00
21 changed files with 339 additions and 119 deletions

View File

@@ -1,32 +0,0 @@
# Python CircleCI 2.0 configuration file
# Updating CircleCI configuration from v1 to v2
# Check https://circleci.com/docs/2.0/language-python/ for more details
#
version: 2
jobs:
build:
machine: true
steps:
- checkout
- run:
name: build images
command: |
docker build -t jupyterhub/jupyterhub .
docker build -t jupyterhub/jupyterhub-onbuild onbuild
docker build -t jupyterhub/jupyterhub:alpine -f dockerfiles/Dockerfile.alpine .
docker build -t jupyterhub/singleuser singleuser
- run:
name: smoke test jupyterhub
command: |
docker run --rm -it jupyterhub/jupyterhub jupyterhub --help
- run:
name: verify static files
command: |
docker run --rm -it -v $PWD/dockerfiles:/io jupyterhub/jupyterhub python3 /io/test.py
# Tell CircleCI to use this workflow when it builds the site
workflows:
version: 2
default:
jobs:
- build

View File

@@ -66,3 +66,120 @@ jobs:
run: |
pip install twine
twine upload --skip-existing dist/*
publish-docker:
runs-on: ubuntu-20.04
services:
# So that we can test this in PRs/branches
local-registry:
image: registry:2
ports:
- 5000:5000
steps:
- name: Should we push this image to a public registry?
run: |
if [ "${{ startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/master') }}" = "true" ]; then
# Empty => Docker Hub
echo "REGISTRY=" >> $GITHUB_ENV
else
echo "REGISTRY=localhost:5000/" >> $GITHUB_ENV
fi
- uses: actions/checkout@v2
# Setup docker to build for multiple platforms, see:
# https://github.com/docker/build-push-action/tree/v2.4.0#usage
# https://github.com/docker/build-push-action/blob/v2.4.0/docs/advanced/multi-platform.md
- name: Set up QEMU (for docker buildx)
uses: docker/setup-qemu-action@25f0500ff22e406f7191a2a8ba8cda16901ca018 # associated tag: v1.0.2
- name: Set up Docker Buildx (for multi-arch builds)
uses: docker/setup-buildx-action@2a4b53665e15ce7d7049afb11ff1f70ff1610609 # associated tag: v1.1.2
with:
# Allows pushing to registry on localhost:5000
driver-opts: network=host
- name: Setup push rights to Docker Hub
# This was setup by...
# 1. Creating a Docker Hub service account "jupyterhubbot"
# 2. Creating a access token for the service account specific to this
# repository: https://hub.docker.com/settings/security
# 3. Making the account part of the "bots" team, and granting that team
# permissions to push to the relevant images:
# https://hub.docker.com/orgs/jupyterhub/teams/bots/permissions
# 4. Registering the username and token as a secret for this repo:
# https://github.com/jupyterhub/jupyterhub/settings/secrets/actions
if: env.REGISTRY != 'localhost:5000/'
run: |
docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKERHUB_TOKEN }}"
# https://github.com/jupyterhub/action-major-minor-tag-calculator
# If this is a tagged build this will return additional parent tags.
# E.g. 1.2.3 is expanded to Docker tags
# [{prefix}:1.2.3, {prefix}:1.2, {prefix}:1, {prefix}:latest] unless
# this is a backported tag in which case the newer tags aren't updated.
# For branches this will return the branch name.
# If GITHUB_TOKEN isn't available (e.g. in PRs) returns no tags [].
- name: Get list of jupyterhub tags
id: jupyterhubtags
uses: jupyterhub/action-major-minor-tag-calculator@v1
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub:"
defaultTag: "${{ env.REGISTRY }}jupyterhub/jupyterhub:noref"
- name: Build and push jupyterhub
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
# tags parameter must be a string input so convert `gettags` JSON
# array into a comma separated list of tags
tags: ${{ join(fromJson(steps.jupyterhubtags.outputs.tags)) }}
# jupyterhub-onbuild
- name: Get list of jupyterhub-onbuild tags
id: onbuildtags
uses: jupyterhub/action-major-minor-tag-calculator@v1
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-onbuild:"
defaultTag: "${{ env.REGISTRY }}jupyterhub/jupyterhub-onbuild:noref"
- name: Build and push jupyterhub-onbuild
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
with:
build-args: |
BASE_IMAGE=${{ fromJson(steps.jupyterhubtags.outputs.tags)[0] }}
context: onbuild
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ join(fromJson(steps.onbuildtags.outputs.tags)) }}
# jupyterhub-demo
- name: Get list of jupyterhub-demo tags
id: demotags
uses: jupyterhub/action-major-minor-tag-calculator@v1
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-demo:"
defaultTag: "${{ env.REGISTRY }}jupyterhub/jupyterhub-demo:noref"
- name: Build and push jupyterhub-demo
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
with:
build-args: |
BASE_IMAGE=${{ fromJson(steps.onbuildtags.outputs.tags)[0] }}
context: demo-image
# linux/arm64 currently fails:
# ERROR: Could not build wheels for argon2-cffi which use PEP 517 and cannot be installed directly
# ERROR: executor failed running [/bin/sh -c python3 -m pip install notebook]: exit code: 1
platforms: linux/amd64
push: true
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}

View File

@@ -222,3 +222,25 @@ jobs:
- name: Submit codecov report
run: |
codecov
docker-build:
runs-on: ubuntu-20.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: build images
run: |
docker build -t jupyterhub/jupyterhub .
docker build -t jupyterhub/jupyterhub-onbuild onbuild
docker build -t jupyterhub/jupyterhub:alpine -f dockerfiles/Dockerfile.alpine .
docker build -t jupyterhub/singleuser singleuser
- name: smoke test jupyterhub
run: |
docker run --rm -t jupyterhub/jupyterhub jupyterhub --help
- name: verify static files
run: |
docker run --rm -t -v $PWD/dockerfiles:/io jupyterhub/jupyterhub python3 /io/test.py

View File

@@ -21,7 +21,7 @@
# your jupyterhub_config.py will be added automatically
# from your docker directory.
ARG BASE_IMAGE=ubuntu:focal-20200729@sha256:6f2fb2f9fb5582f8b587837afd6ea8f37d8d1d9e41168c90f410a6ef15fa8ce5
ARG BASE_IMAGE=ubuntu:focal-20200729
FROM $BASE_IMAGE AS builder
USER root

View File

@@ -3,7 +3,7 @@ swagger: "2.0"
info:
title: JupyterHub
description: The REST API for JupyterHub
version: 1.2.0dev
version: 1.4.0
license:
name: BSD-3-Clause
schemes: [http, https]

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
# Docker Cloud build hooks
These are the hooks

View File

@@ -1,7 +0,0 @@
#!/bin/bash
set -exuo pipefail
# build jupyterhub-onbuild image
docker build --build-arg BASE_IMAGE=$DOCKER_REPO:$DOCKER_TAG -t ${DOCKER_REPO}-onbuild:$DOCKER_TAG onbuild
# build jupyterhub-demo image
docker build --build-arg BASE_IMAGE=${DOCKER_REPO}-onbuild:$DOCKER_TAG -t ${DOCKER_REPO}-demo:$DOCKER_TAG demo-image

View File

@@ -1,42 +0,0 @@
#!/bin/bash
set -exuo pipefail
export ONBUILD=${DOCKER_REPO}-onbuild
export DEMO=${DOCKER_REPO}-demo
export REPOS="${DOCKER_REPO} ${ONBUILD} ${DEMO}"
# push ONBUILD image
docker push $ONBUILD:$DOCKER_TAG
docker push $DEMO:$DOCKER_TAG
function get_hub_version() {
rm -f hub_version
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]}"
latest=0
else
latest=1
fi
}
get_hub_version
for repo in ${REPOS}; do
# when building master, push 0.9.0.dev as well
docker tag $repo:$DOCKER_TAG $repo:$hub_xyz
docker push $repo:$hub_xyz
# when building 0.9.x, push 0.9 as well
docker tag $repo:$DOCKER_TAG $repo:$hub_xy
docker push $repo:$hub_xy
# if building a stable release, tag latest as well
if [[ "$latest" == "1" ]]; then
docker tag $repo:$DOCKER_TAG $repo:latest
docker push $repo:latest
fi
done

View File

@@ -885,6 +885,66 @@ class JupyterHub(Application):
def _hub_prefix_default(self):
return url_path_join(self.base_url, '/hub/')
hub_routespec = Unicode(
"/",
help="""
The routing prefix for the Hub itself.
Override to send only a subset of traffic to the Hub.
Default is to use the Hub as the default route for all requests.
This is necessary for normal jupyterhub operation,
as the Hub must receive requests for e.g. `/user/:name`
when the user's server is not running.
However, some deployments using only the JupyterHub API
may want to handle these events themselves,
in which case they can register their own default target with the proxy
and set e.g. `hub_routespec = /hub/` to serve only the hub's own pages, or even `/hub/api/` for api-only operation.
Note: hub_routespec must include the base_url, if any.
.. versionadded:: 1.4
""",
).tag(config=True)
@default("hub_routespec")
def _default_hub_routespec(self):
# Default routespec for the Hub is the *app* base url
# not the hub URL, so the Hub receives requests for non-running servers
# use `/` with host-based routing so the Hub
# gets requests for all hosts
if self.subdomain_host:
routespec = '/'
else:
routespec = self.base_url
return routespec
@validate("hub_routespec")
def _validate_hub_routespec(self, proposal):
"""ensure leading/trailing / on custom routespec prefix
- trailing '/' always required
- leading '/' required unless using subdomains
"""
routespec = proposal.value
if not routespec.endswith("/"):
routespec = routespec + "/"
if not self.subdomain_host and not routespec.startswith("/"):
routespec = "/" + routespec
return routespec
@observe("hub_routespec")
def _hub_routespec_changed(self, change):
if change.new == change.old:
return
routespec = change.new
if routespec not in {'/', self.base_url}:
self.log.warning(
f"Using custom route for Hub: {routespec}."
" Requests for not-running servers may not be handled."
)
@observe('base_url')
def _update_hub_prefix(self, change):
"""add base URL to hub prefix"""
@@ -1718,6 +1778,7 @@ class JupyterHub(Application):
"""Load the Hub URL config"""
hub_args = dict(
base_url=self.hub_prefix,
routespec=self.hub_routespec,
public_host=self.subdomain_host,
certfile=self.internal_ssl_cert,
keyfile=self.internal_ssl_key,
@@ -1733,17 +1794,15 @@ class JupyterHub(Application):
hub_args['ip'] = self.hub_ip
hub_args['port'] = self.hub_port
# routespec for the Hub is the *app* base url
# not the hub URL, so it receives requests for non-running servers
# use `/` with host-based routing so the Hub
# gets requests for all hosts
host = ''
if self.subdomain_host:
routespec = '/'
else:
routespec = self.base_url
self.hub = Hub(**hub_args)
self.hub = Hub(routespec=routespec, **hub_args)
if not self.subdomain_host:
api_prefix = url_path_join(self.hub.base_url, "api/")
if not api_prefix.startswith(self.hub.routespec):
self.log.warning(
f"Hub API prefix {api_prefix} not on prefix {self.hub.routespec}. "
"The Hub may not receive any API requests from outside."
)
if self.hub_connect_ip:
self.hub.connect_ip = self.hub_connect_ip

View File

@@ -26,10 +26,9 @@ def write_alembic_ini(alembic_ini='alembic.ini', db_url='sqlite:///jupyterhub.sq
Parameters
----------
alembic_ini: str
alembic_ini : str
path to the alembic.ini file that should be written.
db_url: str
db_url : str
The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`.
"""
with open(ALEMBIC_INI_TEMPLATE_PATH) as f:
@@ -58,13 +57,11 @@ def _temp_alembic_ini(db_url):
Parameters
----------
db_url: str
db_url : str
The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`.
Returns
-------
alembic_ini: str
The path to the temporary alembic.ini that we have created.
This file will be cleaned up on exit from the context manager.

View File

@@ -1,3 +1,4 @@
"""Handlers for serving prometheus metrics"""
from prometheus_client import CONTENT_TYPE_LATEST
from prometheus_client import generate_latest
from prometheus_client import REGISTRY
@@ -17,4 +18,7 @@ class MetricsHandler(BaseHandler):
self.write(generate_latest(REGISTRY))
default_handlers = [(r'/metrics$', MetricsHandler)]
default_handlers = [
(r'/metrics$', MetricsHandler),
(r'/api/metrics$', MetricsHandler),
]

View File

@@ -676,4 +676,5 @@ default_handlers = [
(r'/token', TokenPageHandler),
(r'/error/(\d+)', ProxyErrorHandler),
(r'/health$', HealthCheckHandler),
(r'/api/health$', HealthCheckHandler),
]

View File

@@ -32,6 +32,7 @@ from tornado.ioloop import PeriodicCallback
from traitlets import Any
from traitlets import Bool
from traitlets import default
from traitlets import Dict
from traitlets import Instance
from traitlets import Integer
from traitlets import observe
@@ -112,6 +113,26 @@ class Proxy(LoggingConfigurable):
""",
)
extra_routes = Dict(
{},
config=True,
help="""
Additional routes to be maintained in the proxy.
A dictionary with a route specification as key, and
a URL as target. The hub will ensure this route is present
in the proxy.
If the hub is running in host based mode (with
JupyterHub.subdomain_host set), the routespec *must*
have a domain component (example.com/my-url/). If the
hub is not running in host based mode, the routespec
*must not* have a domain component (/my-url/).
Helpful when the hub is running in API-only mode.
""",
)
def start(self):
"""Start the proxy.
@@ -330,7 +351,7 @@ class Proxy(LoggingConfigurable):
route = routes[self.app.hub.routespec]
if route['target'] != hub.host:
self.log.warning(
"Updating default route %s%s", route['target'], hub.host
"Updating Hub route %s%s", route['target'], hub.host
)
futures.append(self.add_hub_route(hub))
@@ -384,6 +405,11 @@ class Proxy(LoggingConfigurable):
)
futures.append(self.add_service(service))
# Add extra routes we've been configured for
for routespec, url in self.extra_routes.items():
good_routes.add(routespec)
futures.append(self.add_route(routespec, url, {'extra': True}))
# Now delete the routes that shouldn't be there
for routespec in routes:
if routespec not in good_routes:
@@ -396,7 +422,7 @@ class Proxy(LoggingConfigurable):
def add_hub_route(self, hub):
"""Add the default route for the Hub"""
self.log.info("Adding default route for Hub: %s => %s", hub.routespec, hub.host)
self.log.info("Adding route for Hub: %s => %s", hub.routespec, hub.host)
return self.add_route(hub.routespec, self.hub.host, {'hub': True})
async def restore_routes(self):

View File

@@ -668,12 +668,15 @@ class HubOAuth(HubAuth):
Parameters
----------
handler (RequestHandler): A tornado RequestHandler
next_url (str): The page to redirect to on successful login
handler : RequestHandler
A tornado RequestHandler
next_url : str
The page to redirect to on successful login
Returns
-------
state (str): The OAuth state that has been stored in the cookie (url safe, base64-encoded)
state : str
The OAuth state that has been stored in the cookie (url safe, base64-encoded)
"""
extra_state = {}
if handler.get_cookie(self.state_cookie_name):
@@ -710,7 +713,8 @@ class HubOAuth(HubAuth):
Parameters
----------
next_url (str): The URL of the page to redirect to on successful login.
next_url : str
The URL of the page to redirect to on successful login.
Returns
-------

View File

@@ -1,5 +1,6 @@
"""Test the JupyterHub entry point"""
import binascii
import logging
import os
import re
import sys
@@ -329,3 +330,41 @@ def test_url_config(hub_config, expected):
# validate additional properties
for key, value in expected.items():
assert getattr(app, key) == value
@pytest.mark.parametrize(
"base_url, hub_routespec, expected_routespec, should_warn, bad_prefix",
[
(None, None, "/", False, False),
("/", "/", "/", False, False),
("/base", "/base", "/base/", False, False),
("/", "/hub", "/hub/", True, False),
(None, "hub/api", "/hub/api/", True, False),
("/base", "/hub/", "/hub/", True, True),
(None, "/hub/api/health", "/hub/api/health/", True, True),
],
)
def test_hub_routespec(
base_url, hub_routespec, expected_routespec, should_warn, bad_prefix, caplog
):
cfg = Config()
if base_url:
cfg.JupyterHub.base_url = base_url
if hub_routespec:
cfg.JupyterHub.hub_routespec = hub_routespec
with caplog.at_level(logging.WARNING):
app = JupyterHub(config=cfg, log=logging.getLogger())
app.init_hub()
hub = app.hub
assert hub.routespec == expected_routespec
if should_warn:
assert "custom route for Hub" in caplog.text
assert hub_routespec in caplog.text
else:
assert "custom route for Hub" not in caplog.text
if bad_prefix:
assert "may not receive" in caplog.text
else:
assert "may not receive" not in caplog.text

View File

@@ -195,6 +195,25 @@ async def test_check_routes(app, username, disable_check_routes):
assert before == after
async def test_extra_routes(app):
proxy = app.proxy
# When using host_routing, it's up to the admin to
# provide routespecs that have a domain in them.
# We don't explicitly validate that here.
if app.subdomain_host:
route_spec = 'example.com/test-extra-routes/'
else:
route_spec = '/test-extra-routes/'
target = 'http://localhost:9999/test'
proxy.extra_routes = {route_spec: target}
await proxy.check_routes(app.users, app._service_map)
routes = await app.proxy.get_all_routes()
assert route_spec in routes
assert routes[route_spec]['target'] == target
@pytest.mark.parametrize(
"routespec",
[

View File

@@ -72,9 +72,8 @@ def check_db_locks(func):
The decorator relies on an instance of JupyterHubApp being the first
argument to the decorated function.
Example
-------
Examples
--------
@check_db_locks
def api_request(app, *api_path, **kwargs):

View File

@@ -2,7 +2,7 @@
If you base a Dockerfile on this image:
FROM jupyterhub/jupyterhub-onbuild:0.6
FROM jupyterhub/jupyterhub-onbuild:1.4.0
...
then your `jupyterhub_config.py` adjacent to your Dockerfile will be loaded into the image and used by JupyterHub.

View File

@@ -10,6 +10,7 @@
{% block login %}
<div id="login-main" class="container">
{% block login_container %}
{% if custom_html %}
{{ custom_html | safe }}
{% elif login_service %}
@@ -83,6 +84,7 @@
</div>
</form>
{% endif %}
{% endblock login_container %}
</div>
{% endblock login %}