mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 15:03:02 +00:00
sync with master
This commit is contained in:
@@ -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
|
117
.github/workflows/release.yml
vendored
117
.github/workflows/release.yml
vendored
@@ -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)) }}
|
||||
|
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
@@ -1,3 +0,0 @@
|
||||
# Docker Cloud build hooks
|
||||
|
||||
These are the hooks
|
@@ -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
|
@@ -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
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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),
|
||||
]
|
||||
|
@@ -676,4 +676,5 @@ default_handlers = [
|
||||
(r'/token', TokenPageHandler),
|
||||
(r'/error/(\d+)', ProxyErrorHandler),
|
||||
(r'/health$', HealthCheckHandler),
|
||||
(r'/api/health$', HealthCheckHandler),
|
||||
]
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
-------
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
[
|
||||
|
@@ -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):
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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 %}
|
||||
|
||||
|
Reference in New Issue
Block a user