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: |
|
run: |
|
||||||
pip install twine
|
pip install twine
|
||||||
twine upload --skip-existing dist/*
|
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
|
- name: Submit codecov report
|
||||||
run: |
|
run: |
|
||||||
codecov
|
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
|
# your jupyterhub_config.py will be added automatically
|
||||||
# from your docker directory.
|
# from your docker directory.
|
||||||
|
|
||||||
ARG BASE_IMAGE=ubuntu:focal-20200729@sha256:6f2fb2f9fb5582f8b587837afd6ea8f37d8d1d9e41168c90f410a6ef15fa8ce5
|
ARG BASE_IMAGE=ubuntu:focal-20200729
|
||||||
FROM $BASE_IMAGE AS builder
|
FROM $BASE_IMAGE AS builder
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
@@ -3,7 +3,7 @@ swagger: "2.0"
|
|||||||
info:
|
info:
|
||||||
title: JupyterHub
|
title: JupyterHub
|
||||||
description: The REST API for JupyterHub
|
description: The REST API for JupyterHub
|
||||||
version: 1.2.0dev
|
version: 1.4.0
|
||||||
license:
|
license:
|
||||||
name: BSD-3-Clause
|
name: BSD-3-Clause
|
||||||
schemes: [http, https]
|
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):
|
def _hub_prefix_default(self):
|
||||||
return url_path_join(self.base_url, '/hub/')
|
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')
|
@observe('base_url')
|
||||||
def _update_hub_prefix(self, change):
|
def _update_hub_prefix(self, change):
|
||||||
"""add base URL to hub prefix"""
|
"""add base URL to hub prefix"""
|
||||||
@@ -1718,6 +1778,7 @@ class JupyterHub(Application):
|
|||||||
"""Load the Hub URL config"""
|
"""Load the Hub URL config"""
|
||||||
hub_args = dict(
|
hub_args = dict(
|
||||||
base_url=self.hub_prefix,
|
base_url=self.hub_prefix,
|
||||||
|
routespec=self.hub_routespec,
|
||||||
public_host=self.subdomain_host,
|
public_host=self.subdomain_host,
|
||||||
certfile=self.internal_ssl_cert,
|
certfile=self.internal_ssl_cert,
|
||||||
keyfile=self.internal_ssl_key,
|
keyfile=self.internal_ssl_key,
|
||||||
@@ -1733,17 +1794,15 @@ class JupyterHub(Application):
|
|||||||
hub_args['ip'] = self.hub_ip
|
hub_args['ip'] = self.hub_ip
|
||||||
hub_args['port'] = self.hub_port
|
hub_args['port'] = self.hub_port
|
||||||
|
|
||||||
# routespec for the Hub is the *app* base url
|
self.hub = Hub(**hub_args)
|
||||||
# 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(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:
|
if self.hub_connect_ip:
|
||||||
self.hub.connect_ip = 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
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
alembic_ini : str
|
||||||
alembic_ini: str
|
|
||||||
path to the alembic.ini file that should be written.
|
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`.
|
The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`.
|
||||||
"""
|
"""
|
||||||
with open(ALEMBIC_INI_TEMPLATE_PATH) as f:
|
with open(ALEMBIC_INI_TEMPLATE_PATH) as f:
|
||||||
@@ -58,13 +57,11 @@ def _temp_alembic_ini(db_url):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
db_url : str
|
||||||
db_url: str
|
|
||||||
The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`.
|
The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
||||||
alembic_ini: str
|
alembic_ini: str
|
||||||
The path to the temporary alembic.ini that we have created.
|
The path to the temporary alembic.ini that we have created.
|
||||||
This file will be cleaned up on exit from the context manager.
|
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 CONTENT_TYPE_LATEST
|
||||||
from prometheus_client import generate_latest
|
from prometheus_client import generate_latest
|
||||||
from prometheus_client import REGISTRY
|
from prometheus_client import REGISTRY
|
||||||
@@ -17,4 +18,7 @@ class MetricsHandler(BaseHandler):
|
|||||||
self.write(generate_latest(REGISTRY))
|
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'/token', TokenPageHandler),
|
||||||
(r'/error/(\d+)', ProxyErrorHandler),
|
(r'/error/(\d+)', ProxyErrorHandler),
|
||||||
(r'/health$', HealthCheckHandler),
|
(r'/health$', HealthCheckHandler),
|
||||||
|
(r'/api/health$', HealthCheckHandler),
|
||||||
]
|
]
|
||||||
|
@@ -32,6 +32,7 @@ from tornado.ioloop import PeriodicCallback
|
|||||||
from traitlets import Any
|
from traitlets import Any
|
||||||
from traitlets import Bool
|
from traitlets import Bool
|
||||||
from traitlets import default
|
from traitlets import default
|
||||||
|
from traitlets import Dict
|
||||||
from traitlets import Instance
|
from traitlets import Instance
|
||||||
from traitlets import Integer
|
from traitlets import Integer
|
||||||
from traitlets import observe
|
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):
|
def start(self):
|
||||||
"""Start the proxy.
|
"""Start the proxy.
|
||||||
|
|
||||||
@@ -330,7 +351,7 @@ class Proxy(LoggingConfigurable):
|
|||||||
route = routes[self.app.hub.routespec]
|
route = routes[self.app.hub.routespec]
|
||||||
if route['target'] != hub.host:
|
if route['target'] != hub.host:
|
||||||
self.log.warning(
|
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))
|
futures.append(self.add_hub_route(hub))
|
||||||
|
|
||||||
@@ -384,6 +405,11 @@ class Proxy(LoggingConfigurable):
|
|||||||
)
|
)
|
||||||
futures.append(self.add_service(service))
|
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
|
# Now delete the routes that shouldn't be there
|
||||||
for routespec in routes:
|
for routespec in routes:
|
||||||
if routespec not in good_routes:
|
if routespec not in good_routes:
|
||||||
@@ -396,7 +422,7 @@ class Proxy(LoggingConfigurable):
|
|||||||
|
|
||||||
def add_hub_route(self, hub):
|
def add_hub_route(self, hub):
|
||||||
"""Add the default route for the 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})
|
return self.add_route(hub.routespec, self.hub.host, {'hub': True})
|
||||||
|
|
||||||
async def restore_routes(self):
|
async def restore_routes(self):
|
||||||
|
@@ -668,12 +668,15 @@ class HubOAuth(HubAuth):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
handler (RequestHandler): A tornado RequestHandler
|
handler : RequestHandler
|
||||||
next_url (str): The page to redirect to on successful login
|
A tornado RequestHandler
|
||||||
|
next_url : str
|
||||||
|
The page to redirect to on successful login
|
||||||
|
|
||||||
Returns
|
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 = {}
|
extra_state = {}
|
||||||
if handler.get_cookie(self.state_cookie_name):
|
if handler.get_cookie(self.state_cookie_name):
|
||||||
@@ -710,7 +713,8 @@ class HubOAuth(HubAuth):
|
|||||||
|
|
||||||
Parameters
|
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
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@@ -729,7 +729,7 @@ class Spawner(LoggingConfigurable):
|
|||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
state: dict
|
state: dict
|
||||||
a JSONable dict of state
|
a JSONable dict of state
|
||||||
"""
|
"""
|
||||||
state = {}
|
state = {}
|
||||||
return state
|
return state
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
"""Test the JupyterHub entry point"""
|
"""Test the JupyterHub entry point"""
|
||||||
import binascii
|
import binascii
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -329,3 +330,41 @@ def test_url_config(hub_config, expected):
|
|||||||
# validate additional properties
|
# validate additional properties
|
||||||
for key, value in expected.items():
|
for key, value in expected.items():
|
||||||
assert getattr(app, key) == value
|
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
|
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(
|
@pytest.mark.parametrize(
|
||||||
"routespec",
|
"routespec",
|
||||||
[
|
[
|
||||||
|
@@ -72,9 +72,8 @@ def check_db_locks(func):
|
|||||||
The decorator relies on an instance of JupyterHubApp being the first
|
The decorator relies on an instance of JupyterHubApp being the first
|
||||||
argument to the decorated function.
|
argument to the decorated function.
|
||||||
|
|
||||||
Example
|
Examples
|
||||||
-------
|
--------
|
||||||
|
|
||||||
@check_db_locks
|
@check_db_locks
|
||||||
def api_request(app, *api_path, **kwargs):
|
def api_request(app, *api_path, **kwargs):
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
If you base a Dockerfile on this image:
|
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.
|
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 %}
|
{% block login %}
|
||||||
<div id="login-main" class="container">
|
<div id="login-main" class="container">
|
||||||
|
{% block login_container %}
|
||||||
{% if custom_html %}
|
{% if custom_html %}
|
||||||
{{ custom_html | safe }}
|
{{ custom_html | safe }}
|
||||||
{% elif login_service %}
|
{% elif login_service %}
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endblock login_container %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock login %}
|
{% endblock login %}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user